completr 0.2.1__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.1 → completr-0.2.2}/Cargo.lock +3 -3
  2. {completr-0.2.1 → completr-0.2.2}/Cargo.toml +1 -1
  3. {completr-0.2.1 → completr-0.2.2}/PKG-INFO +1 -1
  4. {completr-0.2.1 → completr-0.2.2}/crates/completr/Cargo.toml +1 -0
  5. {completr-0.2.1 → completr-0.2.2}/crates/completr/src/client.rs +6 -1
  6. {completr-0.2.1 → completr-0.2.2}/crates/completr/src/database.rs +11 -0
  7. {completr-0.2.1 → completr-0.2.2}/crates/completr/src/store.rs +153 -2
  8. {completr-0.2.1 → completr-0.2.2}/crates/completr-py/python/completr/__init__.pyi +1 -0
  9. {completr-0.2.1 → completr-0.2.2}/crates/completr-py/python/completr/completr.pyi +8 -1
  10. {completr-0.2.1 → completr-0.2.2}/crates/completr-py/src/lib.rs +13 -7
  11. {completr-0.2.1 → completr-0.2.2}/crates/completr-py/tests/test_database.py +13 -0
  12. {completr-0.2.1 → completr-0.2.2}/LICENSE +0 -0
  13. {completr-0.2.1 → completr-0.2.2}/README.md +0 -0
  14. {completr-0.2.1 → completr-0.2.2}/crates/completr/README.md +0 -0
  15. {completr-0.2.1 → completr-0.2.2}/crates/completr/src/blocked.rs +0 -0
  16. {completr-0.2.1 → completr-0.2.2}/crates/completr/src/codec.rs +0 -0
  17. {completr-0.2.1 → completr-0.2.2}/crates/completr/src/dict.rs +0 -0
  18. {completr-0.2.1 → completr-0.2.2}/crates/completr/src/document.rs +0 -0
  19. {completr-0.2.1 → completr-0.2.2}/crates/completr/src/elias_fano.rs +0 -0
  20. {completr-0.2.1 → completr-0.2.2}/crates/completr/src/engine.rs +0 -0
  21. {completr-0.2.1 → completr-0.2.2}/crates/completr/src/fuzzy.rs +0 -0
  22. {completr-0.2.1 → completr-0.2.2}/crates/completr/src/highlight.rs +0 -0
  23. {completr-0.2.1 → completr-0.2.2}/crates/completr/src/hybrid.rs +0 -0
  24. {completr-0.2.1 → completr-0.2.2}/crates/completr/src/inbox.rs +0 -0
  25. {completr-0.2.1 → completr-0.2.2}/crates/completr/src/index.rs +0 -0
  26. {completr-0.2.1 → completr-0.2.2}/crates/completr/src/lib.rs +0 -0
  27. {completr-0.2.1 → completr-0.2.2}/crates/completr/src/postings.rs +0 -0
  28. {completr-0.2.1 → completr-0.2.2}/crates/completr/src/search.rs +0 -0
  29. {completr-0.2.1 → completr-0.2.2}/crates/completr/src/segment/merge.rs +0 -0
  30. {completr-0.2.1 → completr-0.2.2}/crates/completr/src/segment/titles.rs +0 -0
  31. {completr-0.2.1 → completr-0.2.2}/crates/completr/src/segment/words.rs +0 -0
  32. {completr-0.2.1 → completr-0.2.2}/crates/completr/src/segment.rs +0 -0
  33. {completr-0.2.1 → completr-0.2.2}/crates/completr/src/staged.rs +0 -0
  34. {completr-0.2.1 → completr-0.2.2}/crates/completr/src/text.rs +0 -0
  35. {completr-0.2.1 → completr-0.2.2}/crates/completr/src/trie.rs +0 -0
  36. {completr-0.2.1 → completr-0.2.2}/crates/completr/src/vectors.rs +0 -0
  37. {completr-0.2.1 → completr-0.2.2}/crates/completr-py/Cargo.toml +0 -0
  38. {completr-0.2.1 → completr-0.2.2}/crates/completr-py/examples/layers.py +0 -0
  39. {completr-0.2.1 → completr-0.2.2}/crates/completr-py/examples/live_updates.py +0 -0
  40. {completr-0.2.1 → completr-0.2.2}/crates/completr-py/examples/quickstart.py +0 -0
  41. {completr-0.2.1 → completr-0.2.2}/crates/completr-py/python/completr/__init__.py +0 -0
  42. {completr-0.2.1 → completr-0.2.2}/crates/completr-py/python/completr/_async.py +0 -0
  43. {completr-0.2.1 → completr-0.2.2}/crates/completr-py/python/completr/_async.pyi +0 -0
  44. {completr-0.2.1 → completr-0.2.2}/crates/completr-py/python/completr/_errors.py +0 -0
  45. {completr-0.2.1 → completr-0.2.2}/crates/completr-py/python/completr/_types.py +0 -0
  46. {completr-0.2.1 → completr-0.2.2}/crates/completr-py/python/completr/py.typed +0 -0
  47. {completr-0.2.1 → completr-0.2.2}/crates/completr-py/tests/test_allocators.py +0 -0
  48. {completr-0.2.1 → completr-0.2.2}/crates/completr-py/tests/test_api.py +0 -0
  49. {completr-0.2.1 → completr-0.2.2}/crates/completr-py/tests/test_client.py +0 -0
  50. {completr-0.2.1 → completr-0.2.2}/crates/completr-py/tests/test_completr.py +0 -0
  51. {completr-0.2.1 → completr-0.2.2}/crates/completr-py/tests/test_vectors.py +0 -0
  52. {completr-0.2.1 → 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.1"
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.1"
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.1"
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.1"
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.1
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();
@@ -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,
@@ -1543,17 +1543,19 @@ struct Store(completr_rs::BlockingStore);
1543
1543
  #[pymethods]
1544
1544
  impl Store {
1545
1545
  #[new]
1546
- #[pyo3(signature = (url, options = None, cache_dir = None))]
1546
+ #[pyo3(signature = (url, options = None, cache_dir = None, *, tags = None))]
1547
1547
  fn new(
1548
1548
  py: Python<'_>,
1549
1549
  url: &str,
1550
1550
  options: Option<HashMap<String, String>>,
1551
1551
  cache_dir: Option<PathBuf>,
1552
+ tags: Option<HashMap<String, String>>,
1552
1553
  ) -> PyResult<Self> {
1553
1554
  let options = options.unwrap_or_default();
1554
1555
  let store = py
1555
1556
  .detach(|| completr_rs::BlockingStore::open_with(url, options))
1556
- .map_err(to_py_err)?;
1557
+ .map_err(to_py_err)?
1558
+ .with_tags(tags.unwrap_or_default());
1557
1559
  let store = match cache_dir {
1558
1560
  Some(dir) => store.with_cache_dir(dir).map_err(to_py_err)?,
1559
1561
  None => store,
@@ -1637,11 +1639,11 @@ impl Database {
1637
1639
 
1638
1640
  #[pymethods]
1639
1641
  impl Database {
1640
- /// `cache_dir` keeps downloaded segments on local disk, memory-mapped. The keyword settings
1641
- /// 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.
1642
1644
  #[new]
1643
1645
  #[pyo3(signature = (
1644
- url, options = None, cache_dir = None, *,
1646
+ url, options = None, cache_dir = None, *, tags = None,
1645
1647
  min_word_chars = 3, max_edit_distance = 2, fuzzy_prefix_chars = 7, vector_bits = 4, compact_keys = false, build_threads = 1,
1646
1648
  ))]
1647
1649
  #[allow(clippy::too_many_arguments)]
@@ -1650,6 +1652,7 @@ impl Database {
1650
1652
  url: &str,
1651
1653
  options: Option<HashMap<String, String>>,
1652
1654
  cache_dir: Option<PathBuf>,
1655
+ tags: Option<HashMap<String, String>>,
1653
1656
  min_word_chars: u8,
1654
1657
  max_edit_distance: u8,
1655
1658
  fuzzy_prefix_chars: u8,
@@ -1660,7 +1663,8 @@ impl Database {
1660
1663
  let options = options.unwrap_or_default();
1661
1664
  let database = py
1662
1665
  .detach(|| completr_rs::block_on(completr_rs::Database::open(url, options)))
1663
- .map_err(to_py_err)?;
1666
+ .map_err(to_py_err)?
1667
+ .with_tags(tags.unwrap_or_default());
1664
1668
  let database = match cache_dir {
1665
1669
  Some(dir) => database.with_cache_dir(dir).map_err(to_py_err)?,
1666
1670
  None => database,
@@ -2519,7 +2523,7 @@ impl Client {
2519
2523
  /// `None` syncs only on `sync()`. The keyword settings apply to segments this client builds.
2520
2524
  #[new]
2521
2525
  #[pyo3(signature = (
2522
- 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,
2523
2527
  min_word_chars = 3, max_edit_distance = 2, fuzzy_prefix_chars = 7, vector_bits = 4, compact_keys = false, build_threads = 1,
2524
2528
  ))]
2525
2529
  #[allow(clippy::too_many_arguments)]
@@ -2529,6 +2533,7 @@ impl Client {
2529
2533
  sync_every: Option<f64>,
2530
2534
  cache_dir: Option<PathBuf>,
2531
2535
  options: Option<HashMap<String, String>>,
2536
+ tags: Option<HashMap<String, String>>,
2532
2537
  min_word_chars: u8,
2533
2538
  max_edit_distance: u8,
2534
2539
  fuzzy_prefix_chars: u8,
@@ -2548,6 +2553,7 @@ impl Client {
2548
2553
  .sync_every(sync_every.map(|s| Duration::from_secs_f64(s.max(0.001))))
2549
2554
  .cache_dir(cache_dir)
2550
2555
  .storage(options.unwrap_or_default().into_iter().collect())
2556
+ .tags(tags.unwrap_or_default().into_iter().collect())
2551
2557
  .build(build);
2552
2558
  let client = py
2553
2559
  .detach(|| completr_rs::block_on(completr_rs::connect(url, connect)))
@@ -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