Skip to content

[search results, deferred] Score every vector search by cosine similarity, and name scores for it (port of #1598's cosine half) - #1663

Draft
edwinyyyu wants to merge 5 commits into
MemMachine:feat/horizontal-scalingfrom
edwinyyyu:port/cosine-scores-main
Draft

edwinyyyu wants to merge 5 commits into
MemMachine:feat/horizontal-scalingfrom
edwinyyyu:port/cosine-scores-main

Conversation

@edwinyyyu

@edwinyyyu edwinyyyu commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Important

Deferred: nothing is to build on this PR until the choice of similarity metrics is decided. It stays a draft on [declared schema 1/2] (#1628) at c14c06d6e.

Port

The cosine half of #1598, merged into speedkick as abf92a3a4, re-derived on the partitioned store of [vector store scale-out 6] rather than cherry-picked. The rest of #1598 (queries answer record UUIDs and scores, no get, semantic memory's vector_uuid) and all of #1603 are in #1733 already, so this PR carries only the metric removal and its renames. Changes the original could not make are made here: the registry-backed base and its partition handle lose the metric, the finiteness check of a threshold becomes validate_min_cosine_similarity, and the Qdrant and Milvus design documents describe cosine scoring. It sits on [declared schema 1/2] (#1628), so it also takes the metric out of that PR's code: sqlite-vec's and Milvus's score paths and the docstrings that name the threshold.

Purpose of the change

The vector store carried four similarity metrics and shipped one. Every embedder here produces vectors meant to be compared by cosine — OpenAI hard-coded it, Bedrock defaulted to it, SentenceTransformer only reported what the model declared — while every layer that touched a score paid for the other three in direction flags, threshold directions, and per-backend tables mapping the enum onto native metric names.

Description

SimilarityMetric is gone. Scores are cosine similarities in [-1, 1], and the names say so: QueryMatch.score and SearchMatch.score → cosine_similarity, query(score_threshold=) → query(min_cosine_similarity=), which no longer needs a direction to be meaningful and is refused when not finite, as the threshold was. A store and its partitions no longer have a similarity_metric, and a search engine factory takes the dimensions alone. The Bedrock embedder's similarity_metric config key and semantic memory's vector_similarity_metric go with it, and the install and configuration docs drop them.

Two consequences beyond the vector store

The vector graph stores carried a metric per stored embedding, as a companion property beside every vector. That is gone; Node.embeddings holds plain vectors.

NebulaGraph's cosine() cannot take APPROXIMATE and its vector indexes offer only L2 and IP. Cosine similarity between unit vectors is their inner product, so embeddings are normalized on the way in and compared with inner_product() against an IP index: cosine ranking that, unlike cosine(), can be approximate. Normalization happens in _vector_to_gql_literal, the one place any vector becomes a GQL literal, so the stored side and the query side cannot disagree. Its tests skip everywhere, CI included — they need a real NebulaGraph server rather than a testcontainer — so this is the least-exercised part of the change and wants a reviewer who can run against a licensed 5.x server.

Breaking

A configuration that still names similarity_metric or vector_similarity_metric has the key ignored.

Stack

24 open PRs: three independent PRs, and the vector store tree of short parallel branches. Every open PR in the tree has feat/horizontal-scaling as its GitHub base, and the independent PRs have main. The branches are in a fork, and a pull request can target only this repository's branches, so the on column gives the order the PRs build on each other. A stacked PR's diff on GitHub includes the PRs under it until they merge.

Independent of the vector store tree, directly on main:

# PR change on
— #1624 Make no memory request create a project main
— #1786 Refuse a filter value of the wrong type for a datetime column, and answer an invalid list filter with 422 (port of #1620) main
— #1792 Refuse property values that some store refuses or alters where an episode enters main

The vector store tree. Each PR builds on the one in its on column; PRs on the same parent are parallel branches and do not depend on each other. #1631 is closed, superseded by #1733–#1736, which hold its changes split in four, with review changes since. #1675 is closed, superseded by #1627. #1841 is closed: its request packing is dropped, and its cancellation contract is in #1840. #1828 and #1839 sit side by side on the branch; #1839 and #1840 are the event memory recovery stack, and #1684 heads the event memory stack, which its own PRs index, listed because it builds on #1840, its branch also carrying #1828's commits until one of the two merges; #1871 keeps its own Stack section. Until the PRs under it merge, their changes show in a stacked PR's diff.

# PR change on
[vector store scale-out 1/6] #1671 (merged) Remove custom sharding from the Qdrant store (port of #1654) main
[vector store scale-out 2/6] #1733 (merged into feat/horizontal-scaling) Answer vector store queries with record UUIDs and scores, and refuse invalid inputs feat/horizontal-scaling
[vector store scale-out 3/6] #1734 (merged into feat/horizontal-scaling) Arbitrate vector store collections in a SQL registry, with an incarnation per collection life and a purge feat/horizontal-scaling
[vector store scale-out 4/6] #1735 (merged into feat/horizontal-scaling) Move the Qdrant store onto the collection registry feat/horizontal-scaling
[vector store scale-out 5/6] #1736 (merged into feat/horizontal-scaling) Move the Milvus store onto the collection registry, against a Milvus server feat/horizontal-scaling
— #1775 (merged into feat/horizontal-scaling) Accept attempts exhausted in the lifecycle churn contract, and say which Qdrant operations filter on the incarnation feat/horizontal-scaling
— #1779 (merged into feat/horizontal-scaling) Classify Qdrant errors by status code alone feat/horizontal-scaling
— #1813 (merged into feat/horizontal-scaling) Create Qdrant collections with strict mode off feat/horizontal-scaling
— #1820 (merged into feat/horizontal-scaling) List only active projects in projects/list feat/horizontal-scaling
[user properties 1/2] #1702 (merged into feat/horizontal-scaling) Keep undeclared properties out of the vector store feat/horizontal-scaling
[user properties 2/2] #1670 (merged into feat/horizontal-scaling) Remove per-project filterable properties (port of #1606) feat/horizontal-scaling
— #1824 (merged into feat/horizontal-scaling) Drop the unread logical key from the purge queues feat/horizontal-scaling
— #1788 Refuse a repeated record UUID or a non-finite property value at upsert, and state the datetime property contract feat/horizontal-scaling
— #1631 (closed) Superseded by #1733–#1736, which hold its changes split in four, with review changes since —
[vector store scale-out 6] #1627 (merged into feat/horizontal-scaling) Give each vector store a fixed schema and string-keyed partitions, built from the deployment's configuration feat/horizontal-scaling
[declared schema 1/2] #1628 (merged into feat/horizontal-scaling) Make a vector store filter only on the properties it declares feat/horizontal-scaling
[sqlite write transactions 1] #1844 (merged into feat/horizontal-scaling) Take SQLite's write lock at BEGIN in every store on a relational SQLite engine feat/horizontal-scaling
[session storage 1/2] #1622 Create a session's storage with the session, never on a request feat/horizontal-scaling
[session storage 2/2] #1625 Look a partition up with get_partition, and remove open-or-create from both stores and close from the segment store #1622
[selective filter 1] #1871 Route filtered queries by selectivity, and search allowlists exhaustively feat/horizontal-scaling
[search results, deferred] #1663 (this PR) Score every vector search by cosine similarity, and name scores for it (port of #1598's cosine half) #1628
[declared schema 2/2] #1616 Close the filter union, and make negation the complement on every backend feat/horizontal-scaling
[sqlite store fixes 1/7] #1460 Publish vector index files atomically (but not durably) feat/horizontal-scaling
[sqlite store fixes 2/7] #1469 Never reuse a row id in SQLiteVectorStore #1460
[sqlite store fixes 3/7] #1672 Own the search engine's concurrency in the store, not in each engine (port of #1612) #1469
[sqlite store fixes 4/7] #1673 Serialize a partition's writes so the engine sees them in order (port of #1607) #1672
[sqlite store fixes 5/7] #1674 Refuse a pending row replay cannot honor, instead of dropping it (port of #1608) #1673
[sqlite store fixes 6/7] #1675 (closed) Superseded by #1627, which takes SQLite's write lock at BEGIN where the engine is created —
[sqlite store fixes 7/7] #1676 Give every write a fresh row id, so a key names one version (port of #1610) #1674
[sqlite store fixes 8] #1843 Hold the engine lock until a cancelled engine call's thread finishes #1676
[qdrant options] #1618 Let a deployment tune a Qdrant collection's HNSW, optimizers and quantization feat/horizontal-scaling
[milvus options] #1741 Let a deployment tune a Milvus collection's vector index and its searches #1618
— #1841 (closed) Its request packing is dropped, and its cancellation contract is in #1840 —
[property keys] #1828 Refuse an illegal metadata key before any store writes, and reserve one key prefix feat/horizontal-scaling
[event memory recovery 1/2] #1839 Give EventMemory a store's shape, with a handle per partition feat/horizontal-scaling
[event memory recovery 2] #1840 Keep event memory's two stores consistent through failed ingests and forgets #1839
[event memory 1/6] #1684 Add time bounds, sources and expansion to EventMemory (port of #1597, without session) #1840
[schema migration 1/2] #1825 Add memmachine-server migrate, which creates every configured store's schema before servers start feat/horizontal-scaling
[schema migration 2/2] #1826 Make startup check what migrate created, and create nothing at startup #1825

This PR is its one commit, c14c06d6e, stacked on #1628. It stays a draft until the choice of similarity metrics is decided.

Verification

At this PR's head c14c06d6e, on 2026-10-19, its tree is the previous head 6f42ba1ea's with design/vector_store_partition_registry.md listing the lookup as "open", the name it has from #1627 until #1625, where it said "get". Checks: ruff check and ruff format --check clean; ty check clean as CI runs it (uv run --frozen --all-extras ty check --project packages/server); uv lock --check clean. Outside that document the tree is the previous head's, so the test runs below stand.

Before the partition registry document's wording fix: At this PR's head 6f42ba1ea, on 2026-10-19, its tree is the previous head 1d6e17fdc's with #1824 beneath it, which drops the purge queues' unread logical key, and #1627 dropping the same key from the partition registry's tombstones and removing the collection registry design document its partition registry document replaces. Checks: ruff check and ruff format --check clean; ty check clean as CI runs it (uv run --frozen --all-extras ty check --project packages/server); uv lock --check clean. The unit tests pass, 1134 tests (8 skipped).

Before the rebase onto #1824's merge: At this PR's head 1d6e17fdc, on 2026-10-16, its tree is the previous head 4134c43fe's with the configuration's check of a provided tombstone_retention_seconds named _validate_tombstone_retention, where it was _require_retention_floor. Checks: ruff check and ruff format --check clean; ty check clean as CI runs it (uv run --frozen --all-extras ty check --project packages/server); uv lock --check clean. The unit tests pass, 1134 tests (8 skipped).

Before the retention check's rename: At this PR's head 4134c43fe, on 2026-10-16, its tree is the previous head 203be42e7's with every vector store check of a value that raises named validate_, as validate_partition_key already was: validate_declared_types, validate_query_vector, validate_score_threshold, validate_limit, and validate_dimensions from the base, and the PRs' own checks in their turn. Checks: ruff check and ruff format --check clean; ty check clean as CI runs it (uv run --frozen --all-extras ty check --project packages/server); uv lock --check clean. The unit tests pass, 1134 tests (8 skipped).

Before the raising checks took the validate_ prefix: At this PR's head 203be42e7, on 2026-10-16, its tree is the previous head 8866cb86a's with #1627's changes: the VectorStore interface drops vector_store_name, vector_dimensions, similarity_metric, and indexed_properties, and the partition handle drops partition_key; a partition key may be any string of up to 255 bytes without NUL, and the SQLite stores name a partition's tables and index file by a SHA-256 digest of it; and the checks that return a bool are is_valid_identifier and is_valid_filter. Checks: ruff check and ruff format --check clean; ty check clean as CI runs it (uv run --frozen --all-extras ty check --project packages/server); uv lock --check clean. This PR's version of the database manager's check compares the dimensions and declared properties each store was built with. The unit tests pass, 1134 tests (8 skipped).

Before the store's properties left its interface: At this PR's head 8866cb86a, on 2026-10-15: its tree is the previous head 0cc31da14's without #1627's per-partition schema record (PartitionSchema, the registry's and the SQLite stores' schema column, and VectorStorePartitionSchemaMismatchError), and with require_partition_key named validate_partition_key; ruff check and ruff format --check clean; ty check clean as CI runs it (uv run --frozen --all-extras ty check --project packages/server); uv lock --check clean. This PR's own wording of the record in the design documents and data_types.py, which dropped the metric from it, goes with it. The unit tests pass, 1128 tests.

Before the per-partition schema record went: At this PR's head 0cc31da14, on 2026-10-15: its tree is the previous head dd32e27c4's with the partition lookup named open_partition and the segment store's close_partition kept, since #1625 renames the one and removes the other; ruff check and ruff format --check clean; ty check clean as CI runs it (uv run --frozen --all-extras ty check --project packages/server); uv lock --check clean.

Before the lookup's rename moved into #1625: At this PR's head dd32e27c4, on 2026-10-15: its tree is the previous head 3f274304b's without the segment store's close_partition; ruff check and ruff format --check clean; ty check clean as CI runs it (uv run --frozen --all-extras ty check --project packages/server); uv lock --check clean.

Before #1627's first commit took the removal of close_partition: At this PR's head 3f274304b, on 2026-10-15, restacked on #1627's rewritten history: its tree is the previous head 250f6d5a0's; ruff check and ruff format --check clean; ty check clean as CI runs it (uv run --frozen --all-extras ty check --project packages/server); uv lock --check clean.

Before #1627's history was rewritten: At this PR's head 250f6d5a0, on 2026-10-15, rebased onto feat/horizontal-scaling at cf7ae1579, which carries main through #1774: ruff check and ruff format --check clean; ty check clean as CI runs it (uv run --frozen --all-extras ty check --project packages/server); uv lock --check clean; the reranker manager's and the Cohere reranker's unit tests pass, 17 tests.

Before the rebase onto cf7ae1579: At this PR's head 251dc6060, on 2026-10-15, rebased onto feat/horizontal-scaling with #1702 and #1670 merged (101182c18): ruff check and ruff format --check clean; ty check clean as CI runs it (uv run --frozen --all-extras ty check --project packages/server); uv lock --check clean.

Before the rebase onto #1670's merge: At this PR's head 1982ad3d9, on 2026-10-13, rebuilt on #1702's 6631b5c77: ruff check and ruff format --check clean; ty check clean as CI runs it (uv run --frozen --all-extras ty check --project packages/server); uv lock --check clean; the episodic memory and filter tests pass, 476 tests (1 skipped).

Before the rebuild on #1702's filter routing: At this PR's head fb38a726f, on 2026-10-09, on feat/horizontal-scaling with #1813 merged: ruff check and ruff format --check clean; ty check clean as CI runs it (uv run --frozen --all-extras ty check --project packages/server); uv lock --check clean.

Before the rebase onto #1813's merge: At this PR's head 95afd369f, on 2026-10-09: ruff check and ruff format --check clean; ty check clean as CI runs it (uv run --frozen --all-extras ty check --project packages/server); uv lock --check clean. Its tree is the previous #1628 head d6e25ccd3's, which carried #1663 and #1628 together: the server suite without integration tests last passed on that change at 6524b2a62, on 2026-10-08, 2110 tests, and the integration tests of the vector stores, the resource manager, episodic memory, and semantic storage at 54b6acbce, on 2026-10-08, 689 tests, against PostgreSQL 16, Neo4j, Qdrant 1.19.1, and Milvus 2.6.24 in containers; the SQLite stores' tests at 49798fdda, on 2026-10-09, 166 tests.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ESpWYTmCR7X3bJEpoA8SAn

This was referenced Sep 16, 2026
@edwinyyyu
edwinyyyu force-pushed the port/cosine-scores-main branch from 86fde33 to df06603 Compare September 17, 2026 17:39
@edwinyyyu edwinyyyu changed the title [speedkick port 8/17] Answer with cosine scores and uuids, not vectors and stale properties (port of #1598) [vector store 3/16] Answer with cosine scores and uuids, not vectors and stale properties (port of #1598) Sep 17, 2026
@edwinyyyu
edwinyyyu force-pushed the port/cosine-scores-main branch from fb38a72 to 1982ad3 Compare October 14, 2026 00:21
edwinyyyu added a commit to edwinyyyu/MemMachine that referenced this pull request Oct 14, 2026
…he core final state

Open-or-create leaves the vector store interface; the service locator's
tests build the cosine-only search engine of MemMachine#1663, and the SQLite stores'
tests keep the cosine-only metric tests of MemMachine#1663.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@edwinyyyu
edwinyyyu force-pushed the port/cosine-scores-main branch from 1982ad3 to 251dc60 Compare October 15, 2026 05:46
@xiongzubiao
xiongzubiao force-pushed the feat/horizontal-scaling branch from 101182c to cf7ae15 Compare October 15, 2026 15:31
@edwinyyyu
edwinyyyu force-pushed the port/cosine-scores-main branch 8 times, most recently from 4134c43 to 1d6e17f Compare October 16, 2026 22:08
…uilt by the composition root

A vector store was a factory of logical collections, each identified by
a (namespace, name) pair and created with its own dimensions, metric and
declared schema; Qdrant and Milvus shared one native collection among
logical collections of equal configuration, under a name derived from a
hash of that configuration, and the collection registry mapped
(namespace, name) to it.

A store is now one collection, and what it held are its partitions.
`VectorStore` is built with its vector store name, dimensions, similarity
metric and declared properties, and every partition shares them; a
partition's handle answers the metric and the declared properties.
`create_partition(key)`,
`open_or_create_partition(key)`, `open_partition(key)` and
`delete_partition(key)` take one string key, and
`purge_deleted_partitions()` replaces `purge_deleted_collections()`. A
partition is the records carrying its incarnation in the store's native
collection (Qdrant, Milvus) or a pair of tables (the SQLite stores).
"Collection" names only the native Qdrant or Milvus collection, whose
create and delete calls keep their names. `startup()` prepares the
storage a store's partitions share: the native collection and its
indexes, or the SQLite tables. Vector store names match `[a-z0-9_]+` and
are at most 32 bytes, the rule property keys follow, so every name works
on every backend: the Qdrant store names its collection by the vector
store name, and the Milvus store by `sys_` followed by it, since Milvus
requires a leading letter or underscore and names its own internals with
a leading underscore. The hash-derived native names go. A partition key
is any non-empty string of at most 255 bytes free of NUL characters,
since no store names a native resource after one. In the vector store's
utilities, a check that returns a bool is named as a predicate,
`is_valid_identifier` and `is_valid_filter`, and a check that raises
starts with `validate_`: `require_declared_types`, `require_dimensions`,
`require_valid_limit`, `require_valid_query_vector` and
`require_valid_score_threshold` become `validate_declared_types`,
`validate_dimensions`, `validate_limit`, `validate_query_vector` and
`validate_score_threshold`, the Milvus store's
`_require_every_key_accepted` becomes `_validate_every_key_accepted`,
and the configuration's `_require_retention_floor`, which checks a
store's tombstone retention, becomes `_validate_tombstone_retention`.

The collection registry becomes the partition registry, keyed by
partition key within one store. Its tables, `partition_registry_pt` and
`partition_registry_gc`, are shared by every store on a database and
keyed by vector store name, so the registries of several stores share
one database and nothing else. Its reservations, registrations,
unregistration and purge rounds are the collection registry's, with the
partition key in place of (namespace, name): the primary key arbitrates
a reservation, a confirmation or cancellation acts on one incarnation,
a registration fences on `require_current()`, and a purge round claims a
tombstone under a lease with a claim generation and counts its attempts.
The registry records no configuration per partition: every partition
shares its store's dimensions, metric and declared properties. On the
registry-backed base, the storage a partition keeps of its own is
prepared by `_prepare_partition_storage(key, incarnation)`, between its
reservation and its confirmation; the Qdrant and Milvus stores keep
nothing per partition, and a purge round works on one incarnation in the
store's own collection. The errors follow the vocabulary:
`VectorStorePartitionAlreadyExistsError`,
`VectorStorePartitionDeletedError`,
`VectorStorePartitionHandleStaleError` and
`VectorStorePartitionPendingError`.

`DatabaseManager.get_vector_store(backend, vector_store_name=,
vector_dimensions=, similarity_metric=, indexed_properties=)` builds and
caches one store per (backend, vector store name), starting its registry
and then the store; asking for a name again with other dimensions,
another metric or other keys is a `VectorStoreConfigurationError`. The
resource manager starts a purge sweeper for each store the first time it
hands the store out. The Qdrant and Milvus backends name their registry
database with `partition_registry`, in place of `collection_registry`.
The event backend's store is named by a UUIDv5 of its embedder id in the
UUIDv5 of its backend's key in a namespace fixed for the event backend,
written as 32 hexadecimal digits, so neither the key nor the id is
constrained, and stores on different backends never share a name. The
event backend opens a session's partition with
`open_or_create_partition`, which retries the races of a partition
another worker is creating or deleting. Semantic memory's one store is
named `semantic_memory` whatever its embedder and holds every org's
features in one partition, `semantic_memory`.

The SQLite stores change shape only: their registry tables become
`vector_store_sqlite_pt` and `vector_store_sqlite_vec_pt`, keyed by
vector store name and partition key, the pending-operation log is keyed
the same way, a partition's tables and index file are named by the vector
store name and a SHA-256 digest of the partition key, and the stores of
one backend share its engine. No migration is provided:
existing SQLite vector data, and the Qdrant and Milvus collections and
registry tables of earlier releases, are left unread.

The lifecycle contract the registry-backed stores satisfy becomes the
partition lifecycle contract, and the in-memory test store's collection
becomes a partition.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@edwinyyyu
edwinyyyu force-pushed the port/cosine-scores-main branch from 1d6e17f to 6f42ba1 Compare October 19, 2026 17:10
edwinyyyu and others added 4 commits October 19, 2026 10:29
…nd their partitions

The design documents came from MemMachine#1733-MemMachine#1736, where a store holds logical
collections addressed by namespace and name, each with its own
configuration. A store is now one collection, with its dimensions, metric
and declared schema fixed at construction, and a partition is one
tenant's records in it, addressed by key. The documents say so:

- the collection registry document becomes the partition registry
  document: a registry belongs to one store and is addressed by partition
  key; its tables are `partition_registry_pt` and `partition_registry_gc`,
  and a tombstone needs no location, since the incarnation alone finds a
  dead partition's records in the store's native collection; a decision
  records that a store is one collection;
- the Qdrant and Milvus documents lay out one native collection per
  store, named by the vector store name (`sys_` and the name on Milvus),
  created at startup, where a partition's creation makes nothing in the
  backend;
- the overview, isolation, consistency and purge documents speak of
  partitions and `purge_deleted_partitions`, and the overview describes
  the store and its partitions.

The measurements and their conditions are unchanged; the purge document
names the MemMachine#1734 commits they were taken at.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A partition stored every property of a record and filtered on any key,
which made a caller's arbitrary keys part of the store's schema: the
SQLite stores kept them in a JSON column and filtered with json_extract,
Milvus the keys its schema did not declare in a JSON field, and a filter
on a key the store never indexed scanned. Since EventMemory routes a
filter on an undeclared key to the segment store, the vector store need
not hold undeclared keys at all.

A partition now stores the properties its store declares and no others.
`upsert` raises UndeclaredPropertyKeyError before anything is sent for a
record naming an undeclared key, and PropertyTypeMismatchError for a
value of another type than its key declares; `query` raises
UndeclaredPropertyKeyError for a filter naming an undeclared key and
UnsupportedFilterError for a node outside the partition's
`supported_filter_nodes`. Both SQLite stores keep one typed, indexed,
nullable column per declared key on the records table (sql_columns.py);
sqlite-vec 0.1.9 rejects NULL in a vec0 metadata column and a declared
key is optional per record, so that store keeps the columns on the
records table and hands the KNN a `rowid IN (SELECT ...)` allowlist,
evaluating the filter during the search instead of after it. Qdrant
drops the JSON copy and keeps a payload field per declared key; Milvus
drops its JSON field and keeps its typed field per declared key.
Datetimes are stored as microseconds since the epoch where a backend has
no datetime type.

Since every key a filter may name is now indexed, the Qdrant store
creates its collection in strict mode (`unindexed_filtering_retrieve`
and `_update` false, Qdrant Cloud's default): a filter on an unindexed key is refused by the server instead
of scanned for. A leaf whose value is of another type than its key
declares matches nothing, as on the SQL stores; the Qdrant compiler
answers it with a filter no point satisfies, since the server would
refuse the condition for the field's index, and Milvus no longer
compares an int with a float key. Local mode does not record the
setting, so a unit test checks the request and integration tests the
server's answer.

`declared_schema_contract.py` states the contract every backend's test
module runs: which records a filtered search admits, over fixtures small
enough that every backend searches them exactly, checked after each
upsert so an approximate index fails on recall, by name, and not on the
filter.

On the registry-backed stores, the checks are the base handle's: `upsert`
runs validate_declared_properties in place of the type check it ran, and
`query` runs validate_supported_filter against the subclass's
`supported_filter_nodes`, which each subclass now implements. A datetime
column on the SQLite stores has a `tz_<key>` column beside it holding the
UTC offset in seconds, written with the value and read by no filter, so a
stored datetime is the value written, as on Milvus, Qdrant and the segment
store; the microseconds column alone would keep only the instant.

The Qdrant and Milvus design documents describe the declared-only
properties and Qdrant's strict mode.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESpWYTmCR7X3bJEpoA8SAn
A declared datetime on the SQLite stores had a `tz_<key>` column beside its
microseconds column, holding the UTC offset in seconds that no filter
reads. The vector stores keep a datetime property's instant only, as
MemMachine#1736 now does for Milvus and MemMachine#1788 for Qdrant, and the segment store
keeps the offset, so the column, `offset_column_name`, and the value
written to it go: each declared property is one column, and a datetime
stays microseconds since the epoch.

The tests read a stored datetime back as its instant in UTC, and the
roundtrip test checks that the stored instant equals the written one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESpWYTmCR7X3bJEpoA8SAn
Every embedder MemMachine ships produces vectors meant to be compared by
cosine -- OpenAI hard-coded it, Bedrock defaulted to it, SentenceTransformer
only reported what the model declared -- while every layer that touched a
score paid for the other three metrics in direction flags, threshold
directions, and per-backend tables mapping the enum onto native metric
names. `SimilarityMetric` is gone; scores are cosine similarities in
[-1, 1] and the names say so: `QueryMatch.score` and `SearchMatch.score`
become `cosine_similarity`, and `query(score_threshold=)` becomes
`query(min_cosine_similarity=)`, which no longer needs a direction to be
meaningful and is refused when not finite, as the threshold was. A store
and its partitions no longer have a `similarity_metric`, the schema a
partition is registered under no longer records one, and a search engine
factory takes the dimensions alone. The Bedrock embedder's
`similarity_metric` config key and semantic memory's
`vector_similarity_metric` go with it, and the install and configuration
docs drop them.

The vector graph stores carried a metric per stored embedding, as a
companion property beside every vector; that is gone and `Node.embeddings`
holds plain vectors. NebulaGraph's `cosine()` cannot take `APPROXIMATE` and
its vector indexes offer only L2 and IP. Cosine similarity between unit
vectors is their inner product, so embeddings are normalized on the way in
and compared with `inner_product()` against an IP index.

The cosine half of MemMachine#1598 (`abf92a3a4` on speedkick), re-derived on the
one-collection store: the rest of MemMachine#1598 (queries answer record UUIDs and
scores, no `get`, semantic memory's `vector_uuid`) and all of MemMachine#1603 are in
registry-backed base and its partition handle lose the metric, the stores'
own threshold checks become `validate_min_cosine_similarity`, and the
design documents describe cosine scoring and a schema without a metric.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESpWYTmCR7X3bJEpoA8SAn

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant