use std::cell::RefCell;
use std::future::Future;
use std::path::{Path, PathBuf};
use anyhow::{Result, anyhow};
use asupersync::runtime::{Runtime, RuntimeBuilder};
use frankensearch::Cx;
use frankensearch::quill::cass::{
CASS_MERGE_COOLDOWN_MS, CASS_MERGE_SEGMENT_THRESHOLD, CassDocument as QuillCassDocument,
CassMergeStatus,
};
use frankensearch::quill::schema::CASS_SEMANTIC_SCHEMA;
use frankensearch::quill::{QuillConfig, QuillIndex, QuillSearchIndex, SchemaDocument};
pub const QUILL_INDEX_MARKER: &str = "MANIFEST";
thread_local! {
static DRIVER: RefCell<Option<Runtime>> = const { RefCell::new(None) };
}
fn drive<T, F>(call: impl FnOnce(Cx) -> F) -> T
where
F: Future<Output = T>,
{
let runtime = DRIVER
.with(|slot| slot.borrow_mut().take())
.unwrap_or_else(|| {
RuntimeBuilder::current_thread()
.build()
.expect("failed to build Quill sync-bridge runtime")
});
struct RestoreOnDrop(Option<Runtime>);
impl Drop for RestoreOnDrop {
fn drop(&mut self) {
if let Some(runtime) = self.0.take() {
DRIVER.with(|slot| {
let mut slot = slot.borrow_mut();
if slot.is_none() {
*slot = Some(runtime);
}
});
}
}
}
let guard = RestoreOnDrop(Some(runtime));
let output = guard
.0
.as_ref()
.expect("sync-bridge runtime is present for the duration of the call")
.block_on(async {
let cx = Cx::for_request();
call(cx).await
});
drop(guard);
output
}
pub fn refresh_reader(reader: &QuillSearchIndex) -> Result<bool> {
drive(|cx| async move { reader.refresh(&cx).await })
.map_err(|error| anyhow!("refreshing the Quill CASS reader: {error}"))
}
#[derive(Debug, Clone, PartialEq)]
pub struct QuillLexicalDocHit {
pub bm25_score: f32,
pub rank: usize,
pub global_docid: u32,
pub document_id: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct QuillLexicalPage {
pub hits: Vec<QuillLexicalDocHit>,
pub total_count: Option<usize>,
pub doc_count: usize,
}
pub fn search_paginated(
reader: &QuillSearchIndex,
query: &frankensearch::quill::query::Query,
limit: usize,
offset: usize,
exact_count: bool,
) -> Result<QuillLexicalPage> {
let result = drive(|cx| async move {
reader.search_preparsed_paginated(&cx, query, limit, offset, exact_count)
})
.map_err(|error| anyhow!("executing a Quill lexical query: {error}"))?;
Ok(QuillLexicalPage {
hits: result
.hits
.iter()
.enumerate()
.map(|(rank, hit)| QuillLexicalDocHit {
bm25_score: hit.score,
rank,
global_docid: hit.global_docid,
document_id: hit.document_id.clone(),
})
.collect(),
total_count: result
.total_count
.map(|count| usize::try_from(count).unwrap_or(usize::MAX)),
doc_count: usize::try_from(result.doc_count).unwrap_or(usize::MAX),
})
}
pub fn stored_text(
reader: &QuillSearchIndex,
field_ord: u16,
global_docid: u32,
) -> Result<Option<String>> {
let Some(bytes) = reader.stored_field_value(field_ord, global_docid)? else {
return Ok(None);
};
Ok(Some(String::from_utf8(bytes).map_err(|error| {
anyhow!("stored column {field_ord} for doc {global_docid} is not UTF-8: {error}")
})?))
}
pub fn stored_i64(
reader: &QuillSearchIndex,
field_ord: u16,
global_docid: u32,
) -> Result<Option<i64>> {
Ok(stored_numeric_bytes(reader, field_ord, global_docid)?.map(i64::from_le_bytes))
}
pub fn stored_u64(
reader: &QuillSearchIndex,
field_ord: u16,
global_docid: u32,
) -> Result<Option<u64>> {
Ok(stored_numeric_bytes(reader, field_ord, global_docid)?.map(u64::from_le_bytes))
}
fn stored_numeric_bytes(
reader: &QuillSearchIndex,
field_ord: u16,
global_docid: u32,
) -> Result<Option<[u8; 8]>> {
let Some(bytes) = reader.stored_field_value(field_ord, global_docid)? else {
return Ok(None);
};
let width = bytes.len();
Ok(Some(<[u8; 8]>::try_from(bytes.as_slice()).map_err(
|_| {
anyhow!(
"stored numeric column {field_ord} for doc {global_docid} is {width} bytes, not 8"
)
},
)?))
}
#[must_use]
pub fn content_snippet_generator(
terms: &[String],
config: frankensearch::quill::SnippetConfig,
) -> frankensearch::quill::SnippetGenerator {
use frankensearch::quill::{SnippetGenerator, SnippetTerm, schema::Analyzer};
SnippetGenerator::new(
Analyzer::CassHyphenNormalize,
terms
.iter()
.filter(|term| !term.is_empty())
.map(|term| SnippetTerm::new(term.clone(), 1)),
config,
)
}
pub fn open_cass_reader(path: &Path) -> Result<QuillSearchIndex> {
drive(|cx| {
let path = path.to_path_buf();
async move {
QuillSearchIndex::open_with_schema(
&cx,
path,
CASS_SEMANTIC_SCHEMA,
QuillConfig::default(),
)
.await
}
})
.map_err(|error| anyhow!("opening Quill CASS reader at {}: {error}", path.display()))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct QuillCassFields {
pub agent: u16,
pub workspace: u16,
pub workspace_original: u16,
pub source_path: u16,
pub msg_idx: u16,
pub created_at: u16,
pub title: u16,
pub content: u16,
pub title_prefix: u16,
pub content_prefix: u16,
pub preview: u16,
pub source_id: u16,
pub origin_kind: u16,
pub origin_host: u16,
pub conversation_id: u16,
}
impl QuillCassFields {
#[must_use]
pub const fn compiled() -> Self {
use frankensearch::quill::cass::field;
Self {
agent: field::AGENT,
workspace: field::WORKSPACE,
workspace_original: field::WORKSPACE_ORIGINAL,
source_path: field::SOURCE_PATH,
msg_idx: field::MSG_IDX,
created_at: field::CREATED_AT,
title: field::TITLE,
content: field::CONTENT,
title_prefix: field::TITLE_PREFIX,
content_prefix: field::CONTENT_PREFIX,
preview: field::PREVIEW,
source_id: field::SOURCE_ID,
origin_kind: field::ORIGIN_KIND,
origin_host: field::ORIGIN_HOST,
conversation_id: field::CONVERSATION_ID,
}
}
}
impl Default for QuillCassFields {
fn default() -> Self {
Self::compiled()
}
}
pub struct QuillCassIndex {
index: QuillIndex,
directory: PathBuf,
last_merge_ts: i64,
}
impl QuillCassIndex {
pub fn open_or_create(path: &Path) -> Result<Self> {
std::fs::create_dir_all(path)?;
let directory = path.to_path_buf();
let index_exists = path.join(QUILL_INDEX_MARKER).exists();
let index = drive(|cx| {
let directory = directory.clone();
async move {
if index_exists {
QuillIndex::open_with_schema(
&cx,
directory,
CASS_SEMANTIC_SCHEMA,
QuillConfig::default(),
)
.await
} else {
QuillIndex::create_with_schema(
&cx,
directory,
CASS_SEMANTIC_SCHEMA,
QuillConfig::default(),
)
.await
}
}
})
.map_err(|error| {
anyhow!(
"{} Quill CASS index at {}: {error}",
if index_exists { "opening" } else { "creating" },
path.display()
)
})?;
let mut index = Self {
index,
directory: path.to_path_buf(),
last_merge_ts: 0,
};
if !index_exists {
index.commit()?;
}
Ok(index)
}
pub fn add_cass_documents(&mut self, documents: &[QuillCassDocument]) -> Result<()> {
if documents.is_empty() {
return Ok(());
}
let projected: Vec<SchemaDocument> = documents
.iter()
.map(QuillCassDocument::to_schema_document)
.collect();
drive(|cx| {
let projected = &projected;
let index = &self.index;
async move { index.index_schema_documents(&cx, projected).await }
})
.map_err(|error| anyhow!("indexing CASS documents into Quill: {error}"))
}
pub fn commit(&mut self) -> Result<()> {
drive(|cx| {
let index = &self.index;
async move { index.commit(&cx).await }
})
.map(|_| ())
.map_err(|error| anyhow!("committing the Quill CASS index: {error}"))
}
pub fn delete_all(&mut self) -> Result<()> {
drive(|cx| {
let index = &self.index;
async move { index.delete_all(&cx).await }
})
.map_err(|error| anyhow!("clearing the Quill CASS index: {error}"))
}
pub fn reader(&self) -> Result<QuillSearchIndex> {
drive(|cx| {
let directory = self.directory.clone();
async move {
QuillSearchIndex::open_with_schema(
&cx,
directory,
CASS_SEMANTIC_SCHEMA,
QuillConfig::default(),
)
.await
}
})
.map_err(|error| anyhow!("opening the Quill CASS reader: {error}"))
}
pub fn doc_count(&self) -> Result<u64> {
Ok(self.reader()?.doc_count()?)
}
#[must_use]
pub fn path(&self) -> &Path {
&self.directory
}
#[must_use]
pub fn merge_status(&self, segment_count: usize, now_ms: i64) -> CassMergeStatus {
CassMergeStatus {
segment_count,
last_merge_ts: self.last_merge_ts,
ms_since_last_merge: if self.last_merge_ts > 0 {
now_ms - self.last_merge_ts
} else {
-1
},
merge_threshold: CASS_MERGE_SEGMENT_THRESHOLD,
cooldown_ms: CASS_MERGE_COOLDOWN_MS,
}
}
pub fn note_merged(&mut self, now_ms: i64) {
self.last_merge_ts = now_ms;
}
pub fn add_cass_document_refs(
&mut self,
documents: &[frankensearch::quill::cass::CassDocumentRef<'_>],
) -> Result<()> {
if documents.is_empty() {
return Ok(());
}
let owned: Vec<QuillCassDocument> = documents
.iter()
.map(|document| QuillCassDocument {
agent: document.agent.to_owned(),
workspace: document.workspace.map(str::to_owned),
workspace_original: document.workspace_original.map(str::to_owned),
source_path: document.source_path.to_owned(),
msg_idx: document.msg_idx,
created_at: document.created_at,
title: document.title.map(str::to_owned),
content: document.content.to_owned(),
source_id: document.source_id.to_owned(),
origin_kind: document.origin_kind.to_owned(),
origin_host: document.origin_host.map(str::to_owned),
conversation_id: document.conversation_id,
})
.collect();
self.add_cass_documents(&owned)
}
#[must_use]
pub fn segment_count(&self) -> usize {
self.reader()
.ok()
.and_then(|reader| reader.segment_count().ok())
.unwrap_or(0)
}
pub fn optimize_if_idle(&mut self, now_ms: i64) -> Result<bool> {
let segments = self.segment_count();
if !self.merge_status(segments, now_ms).should_merge() {
return Ok(false);
}
self.force_merge()?;
self.note_merged(now_ms);
Ok(true)
}
pub fn force_merge(&mut self) -> Result<()> {
drive(|cx| {
let index = &self.index;
async move {
index
.compact(&cx, frankensearch::quill::CompactionPolicy::default())
.await
}
})
.map(|_| ())
.map_err(|error| anyhow!("compacting the Quill CASS index: {error}"))
}
pub const fn configure_bulk_load_merge_policy(&self) {}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample(source_id: &str, msg_idx: u64, content: &str) -> QuillCassDocument {
QuillCassDocument {
agent: "claude".to_owned(),
workspace: Some("cass".to_owned()),
workspace_original: Some("cass".to_owned()),
source_path: format!("/transcripts/{source_id}.jsonl"),
msg_idx,
created_at: Some(1_700_000_000),
title: Some("bridge session".to_owned()),
content: content.to_owned(),
source_id: source_id.to_owned(),
origin_kind: "local".to_owned(),
origin_host: None,
conversation_id: Some(1),
}
}
#[test]
fn bridge_round_trips_documents_without_an_async_caller() {
let directory = tempfile::tempdir().expect("bridge index directory");
let mut index = QuillCassIndex::open_or_create(directory.path()).expect("open or create");
index
.add_cass_documents(&[
sample("alpha", 0, "the borrow checker rejected this lifetime"),
sample("beta", 1, "tokenizer throughput regressed"),
])
.expect("index documents");
index.commit().expect("commit");
assert_eq!(index.doc_count().expect("doc count"), 2);
}
#[test]
fn bridge_nests_without_reentering_one_runtime() {
let directory = tempfile::tempdir().expect("bridge index directory");
let index = QuillCassIndex::open_or_create(directory.path()).expect("open or create");
let nested = drive(|_cx| async {
index.doc_count()
});
assert_eq!(nested.expect("nested doc count"), 0);
}
}