1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
//! A vector index over a path into a document (`10` section 3).
//!
//! An embedding is a field like any other. A document that carries one at
//! `$.embedding` should be one write, and a nearest neighbour search over the
//! collection should be one call that hands back documents, not ids to go and
//! look up somewhere else. Every other engine makes this two stores joined on
//! the id by the caller, and the join is where the two of them drift apart.
//!
//! ```
//! use yo_doc::{Builder, Docs, Key};
//!
//! let mut docs = Docs::new();
//! docs.create_index("$.lang")?;
//! docs.create_vector_index("$.embedding", 3)?;
//!
//! for (id, lang, v) in [
//! ("a", "en", [1.0, 0.0, 0.0]),
//! ("b", "fr", [0.9, 0.1, 0.0]),
//! ("c", "en", [0.0, 0.0, 1.0]),
//! ] {
//! let mut b = Builder::new();
//! b.begin_object()?;
//! b.key(b"lang")?;
//! b.text(lang)?;
//! b.key(b"embedding")?;
//! b.begin_array()?;
//! for x in v {
//! b.float(x)?;
//! }
//! b.end_array()?;
//! b.end_object()?;
//! let bytes = b.finish()?.to_vec();
//! docs.put_bytes(id.as_bytes(), &bytes)?;
//! }
//!
//! // Nearest overall, which is the French one.
//! let mut best = Vec::new();
//! docs.nearest("$.embedding", &[1.0, 0.05, 0.0], 2, |id, _, _| best.push(id.to_vec()))?;
//! assert_eq!(best[0], b"a".to_vec());
//! assert_eq!(best[1], b"b".to_vec());
//!
//! // Nearest among the English ones, decided inside the scan and not after it.
//! let mut found = Vec::new();
//! let english = [("$.lang", Key::text("en"))];
//! docs.nearest_where("$.embedding", &[1.0, 0.05, 0.0], 2, &english, |id, _, _| {
//! found.push(id.to_vec())
//! })?;
//! assert_eq!(found, [b"a".to_vec(), b"c".to_vec()]);
//! # Ok::<(), yo_common::Error>(())
//! ```
//!
//! # Why this is not a [`PathIndex`](crate::PathIndex)
//!
//! Every other index kind files a document under byte keys, and the lookup is
//! equality or a range over those bytes. Nearness is neither. There is no key a
//! query could ask for, the answer depends on all of the coordinates at once,
//! and the structure that answers it is a partitioned quantised index rather
//! than a table from key to posting list. So a vector index is a
//! [`Collection`], keyed by document id, held in its own list beside the path
//! indexes rather than pretending to be one.
//!
//! It is still the same [`Collection`] a vector set on the wire is, which is Y23
//! again: a document's embedding and a `VADD` land in the same code, so a
//! replace, a zero length vector and a dimension mismatch cannot be answered one
//! way here and another way there.
//!
//! # The filter is the point
//!
//! "The five nearest documents where the language is English" is the question
//! people actually have, and answering it by searching for fifty and then
//! throwing away the ones that are not English is a lottery. The more selective
//! the filter the worse the lottery, and it fails quietly: the answers that come
//! back are real, the ones that should have been there were never ranked.
//!
//! So the filter runs inside the posting scan. Every document carries a 64 bit
//! [`Signature`] over the keys its other indexes filed it under, which sits
//! beside the code in the posting and costs one instruction to test on a word
//! the scan has already loaded. [`nearest_where`](crate::Docs::nearest_where)
//! builds the same signature out of the values the query requires, and a
//! document is worth ranking when its bits cover the query's.
//!
//! Two values can land on the same bit, so the signature can let a document
//! through that does not really match. It can never reject one that does, which
//! is the direction that matters, and the caller's own predicate over the
//! answers settles the rest.
//!
//! Because the tag summarises the other indexes, it goes stale when the set of
//! indexes changes. Declaring or dropping an index therefore rewrites every tag,
//! which is one store per document per vector index with no requantising, rather
//! than leaving a filter that used to work quietly answering nothing.
use ;
use Metric;
use ;
use crateKind;
use crateValue;
/// One path holding an embedding, and the collection its vectors live in.
///
/// The collection is keyed by document id, so an answer from it is a document id
/// and the caller never sees a second numbering.
/// Read the vector at `at` into `into`.
///
/// An array of `dim` numbers and nothing else. A path that holds something of
/// another shape fails the write rather than being skipped, because unlike a
/// scalar index there is no reading of "the document does not have one here":
/// the caller declared a path as an embedding and put something else there.
pub
/// The tag a document with these index key lists carries.
///
/// One bit per path and key pair, which is exactly what
/// [`Docs::nearest_where`](crate::Docs::nearest_where) builds on the other side.
/// A document with an array index files under several keys at one path and gets
/// a bit for each, so a query asking for any one of them still covers it.
pub
/// Set the bit for every key in one index's key list.
///
/// The one place a path and a key become a bit, so that the tag a write puts on
/// a document, the tag a rebuild puts back, and the signature a query is filtered
/// by cannot drift apart.
pub