completr 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. {completr-0.2.0 → completr-0.2.2}/Cargo.lock +3 -3
  2. {completr-0.2.0 → completr-0.2.2}/Cargo.toml +1 -1
  3. {completr-0.2.0 → completr-0.2.2}/PKG-INFO +1 -1
  4. {completr-0.2.0 → completr-0.2.2}/crates/completr/Cargo.toml +1 -0
  5. {completr-0.2.0 → completr-0.2.2}/crates/completr/src/client.rs +6 -1
  6. {completr-0.2.0 → completr-0.2.2}/crates/completr/src/database.rs +11 -0
  7. {completr-0.2.0 → completr-0.2.2}/crates/completr/src/store.rs +153 -2
  8. {completr-0.2.0 → completr-0.2.2}/crates/completr-py/Cargo.toml +3 -0
  9. {completr-0.2.0 → completr-0.2.2}/crates/completr-py/python/completr/__init__.pyi +1 -0
  10. {completr-0.2.0 → completr-0.2.2}/crates/completr-py/python/completr/completr.pyi +8 -1
  11. {completr-0.2.0 → completr-0.2.2}/crates/completr-py/src/lib.rs +15 -8
  12. completr-0.2.2/crates/completr-py/tests/test_allocators.py +20 -0
  13. {completr-0.2.0 → completr-0.2.2}/crates/completr-py/tests/test_database.py +13 -0
  14. {completr-0.2.0 → completr-0.2.2}/LICENSE +0 -0
  15. {completr-0.2.0 → completr-0.2.2}/README.md +0 -0
  16. {completr-0.2.0 → completr-0.2.2}/crates/completr/README.md +0 -0
  17. {completr-0.2.0 → completr-0.2.2}/crates/completr/src/blocked.rs +0 -0
  18. {completr-0.2.0 → completr-0.2.2}/crates/completr/src/codec.rs +0 -0
  19. {completr-0.2.0 → completr-0.2.2}/crates/completr/src/dict.rs +0 -0
  20. {completr-0.2.0 → completr-0.2.2}/crates/completr/src/document.rs +0 -0
  21. {completr-0.2.0 → completr-0.2.2}/crates/completr/src/elias_fano.rs +0 -0
  22. {completr-0.2.0 → completr-0.2.2}/crates/completr/src/engine.rs +0 -0
  23. {completr-0.2.0 → completr-0.2.2}/crates/completr/src/fuzzy.rs +0 -0
  24. {completr-0.2.0 → completr-0.2.2}/crates/completr/src/highlight.rs +0 -0
  25. {completr-0.2.0 → completr-0.2.2}/crates/completr/src/hybrid.rs +0 -0
  26. {completr-0.2.0 → completr-0.2.2}/crates/completr/src/inbox.rs +0 -0
  27. {completr-0.2.0 → completr-0.2.2}/crates/completr/src/index.rs +0 -0
  28. {completr-0.2.0 → completr-0.2.2}/crates/completr/src/lib.rs +0 -0
  29. {completr-0.2.0 → completr-0.2.2}/crates/completr/src/postings.rs +0 -0
  30. {completr-0.2.0 → completr-0.2.2}/crates/completr/src/search.rs +0 -0
  31. {completr-0.2.0 → completr-0.2.2}/crates/completr/src/segment/merge.rs +0 -0
  32. {completr-0.2.0 → completr-0.2.2}/crates/completr/src/segment/titles.rs +0 -0
  33. {completr-0.2.0 → completr-0.2.2}/crates/completr/src/segment/words.rs +0 -0
  34. {completr-0.2.0 → completr-0.2.2}/crates/completr/src/segment.rs +0 -0
  35. {completr-0.2.0 → completr-0.2.2}/crates/completr/src/staged.rs +0 -0
  36. {completr-0.2.0 → completr-0.2.2}/crates/completr/src/text.rs +0 -0
  37. {completr-0.2.0 → completr-0.2.2}/crates/completr/src/trie.rs +0 -0
  38. {completr-0.2.0 → completr-0.2.2}/crates/completr/src/vectors.rs +0 -0
  39. {completr-0.2.0 → completr-0.2.2}/crates/completr-py/examples/layers.py +0 -0
  40. {completr-0.2.0 → completr-0.2.2}/crates/completr-py/examples/live_updates.py +0 -0
  41. {completr-0.2.0 → completr-0.2.2}/crates/completr-py/examples/quickstart.py +0 -0
  42. {completr-0.2.0 → completr-0.2.2}/crates/completr-py/python/completr/__init__.py +0 -0
  43. {completr-0.2.0 → completr-0.2.2}/crates/completr-py/python/completr/_async.py +0 -0
  44. {completr-0.2.0 → completr-0.2.2}/crates/completr-py/python/completr/_async.pyi +0 -0
  45. {completr-0.2.0 → completr-0.2.2}/crates/completr-py/python/completr/_errors.py +0 -0
  46. {completr-0.2.0 → completr-0.2.2}/crates/completr-py/python/completr/_types.py +0 -0
  47. {completr-0.2.0 → completr-0.2.2}/crates/completr-py/python/completr/py.typed +0 -0
  48. {completr-0.2.0 → completr-0.2.2}/crates/completr-py/tests/test_api.py +0 -0
  49. {completr-0.2.0 → completr-0.2.2}/crates/completr-py/tests/test_client.py +0 -0
  50. {completr-0.2.0 → completr-0.2.2}/crates/completr-py/tests/test_completr.py +0 -0
  51. {completr-0.2.0 → completr-0.2.2}/crates/completr-py/tests/test_vectors.py +0 -0
  52. {completr-0.2.0 → completr-0.2.2}/pyproject.toml +0 -0
@@ -735,7 +735,7 @@ dependencies = [
735
735
 
736
736
  [[package]]
737
737
  name = "completr"
738
- version = "0.2.0"
738
+ version = "0.2.2"
739
739
  dependencies = [
740
740
  "arc-swap",
741
741
  "async-trait",
@@ -765,7 +765,7 @@ dependencies = [
765
765
 
766
766
  [[package]]
767
767
  name = "completr-cli"
768
- version = "0.2.0"
768
+ version = "0.2.2"
769
769
  dependencies = [
770
770
  "clap",
771
771
  "completr",
@@ -776,7 +776,7 @@ dependencies = [
776
776
 
777
777
  [[package]]
778
778
  name = "completr-py"
779
- version = "0.2.0"
779
+ version = "0.2.2"
780
780
  dependencies = [
781
781
  "completr",
782
782
  "mimalloc",
@@ -3,7 +3,7 @@ resolver = "2"
3
3
  members = ["crates/completr", "crates/completr-py"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.2.0"
6
+ version = "0.2.2"
7
7
  edition = "2021"
8
8
  rust-version = "1.94.1"
9
9
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: completr
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Operating System :: MacOS
@@ -53,6 +53,7 @@ aws-credentials = ["aws", "dep:aws-config", "dep:aws-credential-types", "dep:asy
53
53
  [dev-dependencies]
54
54
  turbovec = "1.0.0"
55
55
  tempfile = "3"
56
+ async-trait = "0.1"
56
57
  xxhash-rust = { version = "0.8", features = ["xxh3"] }
57
58
  serde_json = { version = "1", features = ["float_roundtrip"] }
58
59
  tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
@@ -28,6 +28,8 @@ pub struct ConnectOptions {
28
28
  pub cache_dir: Option<PathBuf>,
29
29
  /// Storage options, such as credentials or a region, passed to the object store.
30
30
  pub storage: Vec<(String, String)>,
31
+ /// Tags on every object written, e.g. to select bucket lifecycle rules.
32
+ pub tags: Vec<(String, String)>,
31
33
  pub build: BuildOptions,
32
34
  pub index: IndexOptions,
33
35
  }
@@ -38,6 +40,7 @@ impl Default for ConnectOptions {
38
40
  sync_every: Some(Duration::from_secs(5)),
39
41
  cache_dir: None,
40
42
  storage: Vec::new(),
43
+ tags: Vec::new(),
41
44
  build: BuildOptions::default(),
42
45
  index: IndexOptions::default(),
43
46
  }
@@ -48,6 +51,7 @@ crate::setters!(ConnectOptions {
48
51
  sync_every: Option<Duration>,
49
52
  cache_dir: Option<PathBuf>,
50
53
  storage: Vec<(String, String)>,
54
+ tags: Vec<(String, String)>,
51
55
  build: BuildOptions,
52
56
  index: IndexOptions,
53
57
  });
@@ -218,7 +222,8 @@ const LEGACY_SETTINGS: &str = "collection/";
218
222
  pub async fn connect(url: &str, options: ConnectOptions) -> Result<Client, Error> {
219
223
  let mut database = Database::open(url, options.storage)
220
224
  .await?
221
- .with_build_options(options.build);
225
+ .with_build_options(options.build)
226
+ .with_tags(options.tags);
222
227
  if let Some(dir) = &options.cache_dir {
223
228
  database = database.with_cache_dir(dir)?;
224
229
  }
@@ -196,6 +196,17 @@ impl Database {
196
196
  Ok(Self::new(Store::open_with(url, options).await?))
197
197
  }
198
198
 
199
+ /// Tags every object this database writes, e.g. to select bucket lifecycle rules.
200
+ pub fn with_tags<K: AsRef<str>, V: AsRef<str>>(
201
+ self,
202
+ tags: impl IntoIterator<Item = (K, V)>,
203
+ ) -> Self {
204
+ Self {
205
+ store: self.store.with_tags(tags),
206
+ ..self
207
+ }
208
+ }
209
+
199
210
  /// Caches downloaded segments in `dir` and memory-maps them.
200
211
  pub fn with_cache_dir(self, dir: impl AsRef<std::path::Path>) -> Result<Self, Error> {
201
212
  Ok(Self {
@@ -7,7 +7,7 @@ use futures::{StreamExt, TryStreamExt};
7
7
  use object_store::local::LocalFileSystem;
8
8
  use object_store::memory::InMemory;
9
9
  use object_store::path::Path;
10
- use object_store::{ObjectStore, ObjectStoreExt, PutMode, PutOptions, PutPayload};
10
+ use object_store::{ObjectStore, ObjectStoreExt, PutMode, PutOptions, PutPayload, TagSet};
11
11
  use url::Url;
12
12
 
13
13
  use crate::{Error, Segment};
@@ -29,6 +29,8 @@ pub struct Store {
29
29
  /// Where remote segments are downloaded to and memory-mapped from, per store URL.
30
30
  cache: Option<PathBuf>,
31
31
  namespace: String,
32
+ /// Tags on every object written, e.g. to select bucket lifecycle rules.
33
+ tags: TagSet,
32
34
  }
33
35
 
34
36
  impl Store {
@@ -80,6 +82,7 @@ impl Store {
80
82
  local_root: None,
81
83
  cache: None,
82
84
  namespace,
85
+ tags: TagSet::default(),
83
86
  })
84
87
  }
85
88
 
@@ -93,6 +96,7 @@ impl Store {
93
96
  local_root: Some(root),
94
97
  cache: None,
95
98
  namespace: String::new(),
99
+ tags: TagSet::default(),
96
100
  })
97
101
  }
98
102
 
@@ -115,9 +119,31 @@ impl Store {
115
119
  .to_vec())
116
120
  }
117
121
 
122
+ /// Tags every object this store writes from now on; S3 and Azure keep them, other stores ignore them.
123
+ pub fn with_tags<K: AsRef<str>, V: AsRef<str>>(
124
+ mut self,
125
+ tags: impl IntoIterator<Item = (K, V)>,
126
+ ) -> Self {
127
+ let mut set = TagSet::default();
128
+ for (key, value) in tags {
129
+ set.push(key.as_ref(), value.as_ref());
130
+ }
131
+ self.tags = set;
132
+ self
133
+ }
134
+
135
+ /// The tags written with every object, URL-encoded as S3 takes them.
136
+ pub fn tags(&self) -> &str {
137
+ self.tags.encoded()
138
+ }
139
+
118
140
  pub async fn put(&self, key: &str, data: Vec<u8>) -> Result<(), Error> {
141
+ let options = PutOptions {
142
+ tags: self.tags.clone(),
143
+ ..PutOptions::default()
144
+ };
119
145
  self.inner
120
- .put(&self.path(key), PutPayload::from(data))
146
+ .put_opts(&self.path(key), PutPayload::from(data), options)
121
147
  .await?;
122
148
  Ok(())
123
149
  }
@@ -127,6 +153,7 @@ impl Store {
127
153
  pub async fn put_if_absent(&self, key: &str, data: Vec<u8>) -> Result<bool, Error> {
128
154
  let options = PutOptions {
129
155
  mode: PutMode::Create,
156
+ tags: self.tags.clone(),
130
157
  ..PutOptions::default()
131
158
  };
132
159
  match self
@@ -431,6 +458,13 @@ impl BlockingStore {
431
458
  self.0.with_cache_dir(dir).map(Self)
432
459
  }
433
460
 
461
+ pub fn with_tags<K: AsRef<str>, V: AsRef<str>>(
462
+ self,
463
+ tags: impl IntoIterator<Item = (K, V)>,
464
+ ) -> Self {
465
+ Self(self.0.with_tags(tags))
466
+ }
467
+
434
468
  pub fn prune_cache<'a>(&self, keep: impl IntoIterator<Item = &'a str>) -> Result<usize, Error> {
435
469
  self.0.prune_cache(keep)
436
470
  }
@@ -557,6 +591,123 @@ mod tests {
557
591
  use super::*;
558
592
  use crate::Document;
559
593
 
594
+ /// An in-memory store that records the tags each write carries.
595
+ #[derive(Debug, Default)]
596
+ struct Recording {
597
+ inner: InMemory,
598
+ tags: std::sync::Mutex<Vec<String>>,
599
+ }
600
+
601
+ impl std::fmt::Display for Recording {
602
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
603
+ write!(f, "Recording")
604
+ }
605
+ }
606
+
607
+ #[async_trait::async_trait]
608
+ impl ObjectStore for Recording {
609
+ async fn put_opts(
610
+ &self,
611
+ location: &Path,
612
+ payload: PutPayload,
613
+ opts: PutOptions,
614
+ ) -> object_store::Result<object_store::PutResult> {
615
+ self.tags
616
+ .lock()
617
+ .unwrap()
618
+ .push(opts.tags.encoded().to_owned());
619
+ self.inner.put_opts(location, payload, opts).await
620
+ }
621
+
622
+ async fn put_multipart_opts(
623
+ &self,
624
+ location: &Path,
625
+ opts: object_store::PutMultipartOptions,
626
+ ) -> object_store::Result<Box<dyn object_store::MultipartUpload>> {
627
+ self.inner.put_multipart_opts(location, opts).await
628
+ }
629
+
630
+ async fn get_opts(
631
+ &self,
632
+ location: &Path,
633
+ options: object_store::GetOptions,
634
+ ) -> object_store::Result<object_store::GetResult> {
635
+ self.inner.get_opts(location, options).await
636
+ }
637
+
638
+ fn delete_stream(
639
+ &self,
640
+ locations: futures::stream::BoxStream<'static, object_store::Result<Path>>,
641
+ ) -> futures::stream::BoxStream<'static, object_store::Result<Path>> {
642
+ self.inner.delete_stream(locations)
643
+ }
644
+
645
+ fn list(
646
+ &self,
647
+ prefix: Option<&Path>,
648
+ ) -> futures::stream::BoxStream<'static, object_store::Result<object_store::ObjectMeta>>
649
+ {
650
+ self.inner.list(prefix)
651
+ }
652
+
653
+ async fn list_with_delimiter(
654
+ &self,
655
+ prefix: Option<&Path>,
656
+ ) -> object_store::Result<object_store::ListResult> {
657
+ self.inner.list_with_delimiter(prefix).await
658
+ }
659
+
660
+ async fn copy_opts(
661
+ &self,
662
+ from: &Path,
663
+ to: &Path,
664
+ options: object_store::CopyOptions,
665
+ ) -> object_store::Result<()> {
666
+ self.inner.copy_opts(from, to, options).await
667
+ }
668
+ }
669
+
670
+ #[tokio::test]
671
+ async fn every_write_carries_the_tags() {
672
+ let recording = Arc::new(Recording::default());
673
+ let store = Store {
674
+ inner: recording.clone(),
675
+ prefix: Path::default(),
676
+ local_root: None,
677
+ cache: None,
678
+ namespace: "recording".into(),
679
+ tags: TagSet::default(),
680
+ }
681
+ .with_tags([
682
+ ("LifecycleRule", "KeepForever"),
683
+ ("team", "search & ranking"),
684
+ ]);
685
+ assert_eq!(
686
+ store.tags(),
687
+ "LifecycleRule=KeepForever&team=search+%26+ranking"
688
+ );
689
+
690
+ let database = crate::Database::new(store);
691
+ let mut txn = database.begin().await.unwrap();
692
+ txn.append_documents("songs", [Document::new(1, "Rust", 0.5)], [])
693
+ .unwrap();
694
+ txn.commit().await.unwrap();
695
+ let mut changes = crate::ChangeSet::new();
696
+ changes.upsert("songs", [Document::new(2, "Go", 0.5)]);
697
+ database.submit(changes).await.unwrap();
698
+ database
699
+ .acquire_lease("ingestor", "me", std::time::Duration::from_secs(60))
700
+ .await
701
+ .unwrap();
702
+
703
+ let written = recording.tags.lock().unwrap().clone();
704
+ // A segment, a version, a change set and a lease.
705
+ assert!(written.len() >= 4, "{written:?}");
706
+ assert!(written
707
+ .iter()
708
+ .all(|t| t == "LifecycleRule=KeepForever&team=search+%26+ranking"));
709
+ }
710
+
560
711
  #[test]
561
712
  fn local_and_memory_round_trip() {
562
713
  let dir = tempfile::tempdir().unwrap();
@@ -18,4 +18,7 @@ pyo3 = { version = "0.29", features = ["extension-module", "abi3-py311"] }
18
18
  pyo3-log = "0.13"
19
19
  tracing = { version = "0.1", default-features = false, features = ["log"] }
20
20
  completr_rs = { package = "completr", path = "../completr", features = ["aws-credentials", "gcp", "azure"] }
21
+
22
+ # Not on macOS, where every mimalloc in a process shares fixed thread slots and pyarrow ships one.
23
+ [target.'cfg(target_os = "linux")'.dependencies]
21
24
  mimalloc = { version = "0.1", default-features = false }
@@ -21,6 +21,7 @@ def connect(
21
21
  options: Mapping[str, str] | None = None,
22
22
  cache_dir: str | PathLike[str] | None = None,
23
23
  *,
24
+ tags: Mapping[str, str] | None = None,
24
25
  min_word_chars: int = 3,
25
26
  max_edit_distance: int = 2,
26
27
  fuzzy_prefix_chars: int = 7,
@@ -311,7 +311,12 @@ class Engine:
311
311
 
312
312
  class Store:
313
313
  def __init__(
314
- self, url: str, options: Mapping[str, str] | None = None, cache_dir: str | PathLike[str] | None = None
314
+ self,
315
+ url: str,
316
+ options: Mapping[str, str] | None = None,
317
+ cache_dir: str | PathLike[str] | None = None,
318
+ *,
319
+ tags: Mapping[str, str] | None = None,
315
320
  ) -> None: ...
316
321
  def prune_cache(self, keep: Sequence[str]) -> int: ...
317
322
  def get(self, key: str) -> bytes: ...
@@ -394,6 +399,7 @@ class Database:
394
399
  options: Mapping[str, str] | None = None,
395
400
  cache_dir: str | PathLike[str] | None = None,
396
401
  *,
402
+ tags: Mapping[str, str] | None = None,
397
403
  min_word_chars: int = 3,
398
404
  max_edit_distance: int = 2,
399
405
  fuzzy_prefix_chars: int = 7,
@@ -564,6 +570,7 @@ class Client:
564
570
  sync_every: float | None = 5.0,
565
571
  cache_dir: str | PathLike[str] | None = None,
566
572
  options: Mapping[str, str] | None = None,
573
+ tags: Mapping[str, str] | None = None,
567
574
  min_word_chars: int = 3,
568
575
  max_edit_distance: int = 2,
569
576
  fuzzy_prefix_chars: int = 7,
@@ -10,7 +10,8 @@ use pyo3::prelude::*;
10
10
  use pyo3::sync::PyOnceLock;
11
11
  use pyo3::types::{PyBytes, PyDict, PyList, PyString, PyType};
12
12
 
13
- // Returns freed memory to the system, which the platform allocator keeps after a build.
13
+ // Returns freed memory to the system, which glibc keeps after a build.
14
+ #[cfg(target_os = "linux")]
14
15
  #[global_allocator]
15
16
  static ALLOCATOR: mimalloc::MiMalloc = mimalloc::MiMalloc;
16
17
 
@@ -1542,17 +1543,19 @@ struct Store(completr_rs::BlockingStore);
1542
1543
  #[pymethods]
1543
1544
  impl Store {
1544
1545
  #[new]
1545
- #[pyo3(signature = (url, options = None, cache_dir = None))]
1546
+ #[pyo3(signature = (url, options = None, cache_dir = None, *, tags = None))]
1546
1547
  fn new(
1547
1548
  py: Python<'_>,
1548
1549
  url: &str,
1549
1550
  options: Option<HashMap<String, String>>,
1550
1551
  cache_dir: Option<PathBuf>,
1552
+ tags: Option<HashMap<String, String>>,
1551
1553
  ) -> PyResult<Self> {
1552
1554
  let options = options.unwrap_or_default();
1553
1555
  let store = py
1554
1556
  .detach(|| completr_rs::BlockingStore::open_with(url, options))
1555
- .map_err(to_py_err)?;
1557
+ .map_err(to_py_err)?
1558
+ .with_tags(tags.unwrap_or_default());
1556
1559
  let store = match cache_dir {
1557
1560
  Some(dir) => store.with_cache_dir(dir).map_err(to_py_err)?,
1558
1561
  None => store,
@@ -1636,11 +1639,11 @@ impl Database {
1636
1639
 
1637
1640
  #[pymethods]
1638
1641
  impl Database {
1639
- /// `cache_dir` keeps downloaded segments on local disk, memory-mapped. The keyword settings
1640
- /// apply to segments this database builds from documents.
1642
+ /// `cache_dir` keeps downloaded segments on local disk, memory-mapped; `tags` go on every object
1643
+ /// written. The other keyword settings apply to segments this database builds from documents.
1641
1644
  #[new]
1642
1645
  #[pyo3(signature = (
1643
- url, options = None, cache_dir = None, *,
1646
+ url, options = None, cache_dir = None, *, tags = None,
1644
1647
  min_word_chars = 3, max_edit_distance = 2, fuzzy_prefix_chars = 7, vector_bits = 4, compact_keys = false, build_threads = 1,
1645
1648
  ))]
1646
1649
  #[allow(clippy::too_many_arguments)]
@@ -1649,6 +1652,7 @@ impl Database {
1649
1652
  url: &str,
1650
1653
  options: Option<HashMap<String, String>>,
1651
1654
  cache_dir: Option<PathBuf>,
1655
+ tags: Option<HashMap<String, String>>,
1652
1656
  min_word_chars: u8,
1653
1657
  max_edit_distance: u8,
1654
1658
  fuzzy_prefix_chars: u8,
@@ -1659,7 +1663,8 @@ impl Database {
1659
1663
  let options = options.unwrap_or_default();
1660
1664
  let database = py
1661
1665
  .detach(|| completr_rs::block_on(completr_rs::Database::open(url, options)))
1662
- .map_err(to_py_err)?;
1666
+ .map_err(to_py_err)?
1667
+ .with_tags(tags.unwrap_or_default());
1663
1668
  let database = match cache_dir {
1664
1669
  Some(dir) => database.with_cache_dir(dir).map_err(to_py_err)?,
1665
1670
  None => database,
@@ -2518,7 +2523,7 @@ impl Client {
2518
2523
  /// `None` syncs only on `sync()`. The keyword settings apply to segments this client builds.
2519
2524
  #[new]
2520
2525
  #[pyo3(signature = (
2521
- url = "memory://", *, sync_every = Some(5.0), cache_dir = None, options = None,
2526
+ url = "memory://", *, sync_every = Some(5.0), cache_dir = None, options = None, tags = None,
2522
2527
  min_word_chars = 3, max_edit_distance = 2, fuzzy_prefix_chars = 7, vector_bits = 4, compact_keys = false, build_threads = 1,
2523
2528
  ))]
2524
2529
  #[allow(clippy::too_many_arguments)]
@@ -2528,6 +2533,7 @@ impl Client {
2528
2533
  sync_every: Option<f64>,
2529
2534
  cache_dir: Option<PathBuf>,
2530
2535
  options: Option<HashMap<String, String>>,
2536
+ tags: Option<HashMap<String, String>>,
2531
2537
  min_word_chars: u8,
2532
2538
  max_edit_distance: u8,
2533
2539
  fuzzy_prefix_chars: u8,
@@ -2547,6 +2553,7 @@ impl Client {
2547
2553
  .sync_every(sync_every.map(|s| Duration::from_secs_f64(s.max(0.001))))
2548
2554
  .cache_dir(cache_dir)
2549
2555
  .storage(options.unwrap_or_default().into_iter().collect())
2556
+ .tags(tags.unwrap_or_default().into_iter().collect())
2550
2557
  .build(build);
2551
2558
  let client = py
2552
2559
  .detach(|| completr_rs::block_on(completr_rs::connect(url, connect)))
@@ -0,0 +1,20 @@
1
+ import pytest
2
+
3
+ import completr
4
+
5
+ pyarrow = pytest.importorskip("pyarrow")
6
+
7
+
8
+ def test_completes_alongside_pyarrow_allocations(tmp_path):
9
+ pool = pyarrow.default_memory_pool()
10
+ table = pyarrow.table({"text": [f"skill {i}" for i in range(10_000)]})
11
+ assert pool.bytes_allocated() > 0
12
+ db = completr.connect(str(tmp_path / "db"))
13
+ with db.namespace("en-US").begin() as txn:
14
+ txn.overwrite("owner", [{"id": f"s{i}", "text": f"Skill {i}", "popularity": 0.5} for i in range(2_000)])
15
+ namespace = completr.connect(str(tmp_path / "db")).engine(sync_every=None).namespace("en-US")
16
+ for _ in range(200):
17
+ hits = namespace.complete("skill 1", ["owner"])
18
+ assert [str(h.id) for h in hits]
19
+ table = table.slice(1)
20
+ assert len(table) > 0
@@ -3,6 +3,8 @@ import time
3
3
 
4
4
  import pytest
5
5
 
6
+ import completr
7
+
6
8
  from completr import ConflictError
7
9
  from completr import ChangeSet, Database, Engine, Replica, Store, Ingestor
8
10
 
@@ -184,3 +186,14 @@ def test_threaded_queries_during_updates(database):
184
186
  assert len({id(index) for index, _, _ in samples}) > 1
185
187
  for index, query, result in samples:
186
188
  assert [(h.id, h.score, h.kind) for h in index.complete(query, 10)] == result
189
+
190
+
191
+ def test_tags_are_accepted_everywhere_and_ignored_by_local_stores(tmp_path):
192
+ tags = {"LifecycleRule": "KeepForever"}
193
+ db = completr.connect(str(tmp_path / "db"), tags=tags)
194
+ with db.begin() as txn:
195
+ txn.append("songs", DOCS[:2])
196
+ assert db.index_names() == ["songs"]
197
+ completr.Store(str(tmp_path / "store"), tags=tags).put("a", b"x")
198
+ client = completr.Client(str(tmp_path / "client"), sync_every=None, tags=tags)
199
+ client.create_collection("songs").add(DOCS[:2])
File without changes
File without changes
File without changes