misukisu commited on
Commit
c0141d6
·
verified ·
1 Parent(s): 02dbc9e

Create indexer.rs

Browse files
Files changed (1) hide show
  1. src/indexer.rs +236 -0
src/indexer.rs ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use crate::parser::ParsedDocument;
2
+ use anyhow::{Context, Result};
3
+ use chrono::Utc;
4
+ use serde::{Deserialize, Serialize};
5
+ use std::fs;
6
+ use std::path::{Path, PathBuf};
7
+ use std::sync::Arc;
8
+ use tantivy::collector::TopDocs;
9
+ use tantivy::query::{BooleanQuery, Occur, Query, QueryParser, TermQuery};
10
+ use tantivy::schema::*;
11
+ use tantivy::snippet::SnippetGenerator;
12
+ use tantivy::{doc, Index, IndexReader, IndexWriter, ReloadPolicy, TantivyDocument, Term};
13
+ use tokio::sync::RwLock;
14
+
15
+ #[derive(Clone)]
16
+ pub struct IndexSchemaFields {
17
+ pub id: Field,
18
+ pub collection: Field,
19
+ pub path: Field,
20
+ pub title: Field,
21
+ pub content: Field,
22
+ pub extension: Field,
23
+ pub file_size: Field,
24
+ pub indexed_at: Field,
25
+ }
26
+
27
+ #[derive(Serialize, Deserialize, Debug, Clone)]
28
+ pub struct SearchHit {
29
+ pub score: f32,
30
+ pub collection: String,
31
+ pub path: String,
32
+ pub title: String,
33
+ pub snippet: String,
34
+ pub extension: String,
35
+ pub file_size: u64,
36
+ }
37
+
38
+ #[derive(Serialize, Deserialize, Debug, Clone)]
39
+ pub struct CollectionStats {
40
+ pub collection_name: String,
41
+ pub doc_count: u64,
42
+ }
43
+
44
+ #[derive(Serialize, Deserialize, Debug, Clone)]
45
+ pub struct IndexStats {
46
+ pub total_documents: u64,
47
+ pub num_segments: usize,
48
+ pub index_path: String,
49
+ pub collections: Vec<CollectionStats>,
50
+ }
51
+
52
+ pub struct TantivyEngine {
53
+ index: Index,
54
+ reader: IndexReader,
55
+ writer: Arc<RwLock<IndexWriter>>,
56
+ fields: IndexSchemaFields,
57
+ base_path: PathBuf,
58
+ }
59
+
60
+ impl TantivyEngine {
61
+ pub fn new<P: AsRef<Path>>(base_path: P) -> Result<Self> {
62
+ let path_buf = base_path.as_ref().to_path_buf();
63
+ fs::create_dir_all(&path_buf)?;
64
+
65
+ let mut schema_builder = Schema::builder();
66
+ let id = schema_builder.add_text_field("id", STRING | STORED);
67
+ let collection = schema_builder.add_text_field("collection", STRING | STORED | FAST);
68
+ let path = schema_builder.add_text_field("path", STRING | STORED);
69
+ let title = schema_builder.add_text_field("title", TEXT | STORED);
70
+ let content = schema_builder.add_text_field("content", TEXT | STORED);
71
+ let extension = schema_builder.add_text_field("extension", STRING | STORED);
72
+ let file_size = schema_builder.add_u64_field("file_size", STORED | FAST);
73
+ let indexed_at = schema_builder.add_i64_field("indexed_at", STORED | FAST);
74
+
75
+ let schema = schema_builder.build();
76
+
77
+ let index = Index::open_or_create(
78
+ tantivy::directory::MmapDirectory::open(&path_buf)?,
79
+ schema.clone(),
80
+ )?;
81
+
82
+ // 100MB memory budget for the writer buffer
83
+ let writer = index.writer(100 * 1024 * 1024)?;
84
+ let reader = index
85
+ .reader_builder()
86
+ .reload_policy(ReloadPolicy::OnCommitWithDelay)
87
+ .try_into()?;
88
+
89
+ let fields = IndexSchemaFields {
90
+ id,
91
+ collection,
92
+ path,
93
+ title,
94
+ content,
95
+ extension,
96
+ file_size,
97
+ indexed_at,
98
+ };
99
+
100
+ Ok(Self {
101
+ index,
102
+ reader,
103
+ writer: Arc::new(RwLock::new(writer)),
104
+ fields,
105
+ base_path: path_buf,
106
+ })
107
+ }
108
+
109
+ pub async fn add_documents(&self, collection_name: &str, docs: Vec<ParsedDocument>) -> Result<usize> {
110
+ let count = docs.len();
111
+ let writer = self.writer.write().await;
112
+
113
+ for doc in docs {
114
+ let mut tantivy_doc = TantivyDocument::default();
115
+ let doc_id = uuid::Uuid::new_v4().to_string();
116
+
117
+ tantivy_doc.add_text(self.fields.id, &doc_id);
118
+ tantivy_doc.add_text(self.fields.collection, collection_name);
119
+ tantivy_doc.add_text(self.fields.path, &doc.path);
120
+ tantivy_doc.add_text(self.fields.title, &doc.title);
121
+ tantivy_doc.add_text(self.fields.content, &doc.content);
122
+ tantivy_doc.add_text(self.fields.extension, &doc.extension);
123
+ tantivy_doc.add_u64(self.fields.file_size, doc.size_bytes);
124
+ tantivy_doc.add_i64(self.fields.indexed_at, Utc::now().timestamp());
125
+
126
+ writer.add_document(tantivy_doc)?;
127
+ }
128
+
129
+ // Commit and reload index reader
130
+ let mut writer_guard = writer;
131
+ writer_guard.commit()?;
132
+ drop(writer_guard);
133
+
134
+ self.reader.reload()?;
135
+ Ok(count)
136
+ }
137
+
138
+ pub fn search(
139
+ &self,
140
+ query_str: &str,
141
+ collection_filter: Option<&str>,
142
+ limit: usize,
143
+ ) -> Result<Vec<SearchHit>> {
144
+ let searcher = self.reader.searcher();
145
+ let query_parser = QueryParser::for_index(
146
+ &self.index,
147
+ vec![self.fields.title, self.fields.content],
148
+ );
149
+
150
+ let parsed_query = query_parser
151
+ .parse_query(query_str)
152
+ .context("Query parse error")?;
153
+
154
+ let final_query: Box<dyn Query> = if let Some(coll) = collection_filter {
155
+ let coll_term = Term::from_field_text(self.fields.collection, coll);
156
+ let coll_query = TermQuery::new(coll_term, IndexRecordOption::Basic);
157
+
158
+ Box::new(BooleanQuery::new(vec![
159
+ (Occur::Must, parsed_query),
160
+ (Occur::Must, Box::new(coll_query)),
161
+ ]))
162
+ } else {
163
+ parsed_query
164
+ };
165
+
166
+ let top_docs = searcher.search(&final_query, &TopDocs::with_limit(limit))?;
167
+ let mut snippet_generator =
168
+ SnippetGenerator::create(&searcher, &*final_query, self.fields.content)?;
169
+ snippet_generator.set_max_num_chars(250);
170
+
171
+ let mut hits = Vec::with_capacity(top_docs.len());
172
+
173
+ for (score, doc_address) in top_docs {
174
+ let retrieved_doc: TantivyDocument = searcher.doc(doc_address)?;
175
+
176
+ let coll_val = retrieved_doc
177
+ .get_first(self.fields.collection)
178
+ .and_then(|v| v.as_str())
179
+ .unwrap_or("default")
180
+ .to_string();
181
+
182
+ let path_val = retrieved_doc
183
+ .get_first(self.fields.path)
184
+ .and_then(|v| v.as_str())
185
+ .unwrap_or("")
186
+ .to_string();
187
+
188
+ let title_val = retrieved_doc
189
+ .get_first(self.fields.title)
190
+ .and_then(|v| v.as_str())
191
+ .unwrap_or("")
192
+ .to_string();
193
+
194
+ let ext_val = retrieved_doc
195
+ .get_first(self.fields.extension)
196
+ .and_then(|v| v.as_str())
197
+ .unwrap_or("")
198
+ .to_string();
199
+
200
+ let size_val = retrieved_doc
201
+ .get_first(self.fields.file_size)
202
+ .and_then(|v| v.as_u64())
203
+ .unwrap_or(0);
204
+
205
+ let snippet = snippet_generator.snippet_from_doc(&retrieved_doc).to_html();
206
+
207
+ hits.push(SearchHit {
208
+ score,
209
+ collection: coll_val,
210
+ path: path_val,
211
+ title: title_val,
212
+ snippet,
213
+ extension: ext_val,
214
+ file_size: size_val,
215
+ });
216
+ }
217
+
218
+ Ok(hits)
219
+ }
220
+
221
+ pub fn get_stats(&self) -> Result<IndexStats> {
222
+ let searcher = self.reader.searcher();
223
+ let total_docs = searcher.num_docs();
224
+ let num_segments = searcher.segment_readers().len();
225
+
226
+ Ok(IndexStats {
227
+ total_documents: total_docs,
228
+ num_segments,
229
+ index_path: self.base_path.to_string_lossy().to_string(),
230
+ collections: vec![CollectionStats {
231
+ collection_name: "global".to_string(),
232
+ doc_count: total_docs,
233
+ }],
234
+ })
235
+ }
236
+ }