2026-06-02 15:22:59 +00:00
use std ::collections ::HashMap ;
use std ::fs ;
use std ::path ::{ Path , PathBuf } ;
use std ::sync ::Mutex ;
use axum ::Json ;
use log ::{ error , info , warn } ;
use once_cell ::sync ::Lazy ;
2026-09-09 16:43:37 +00:00
use qdrant_edge ::external ::serde_json ::{ json , Value } ;
2026-06-02 15:22:59 +00:00
use qdrant_edge ::external ::uuid ::Uuid ;
use qdrant_edge ::{
Condition , Distance , EdgeConfig , EdgeOptimizersConfig , EdgeShard , EdgeVectorParams ,
2026-09-09 16:43:37 +00:00
FieldCondition , Filter , HnswIndexConfig , Match , MatchValue , NamedQuery , Payload , PointId ,
PointInsertOperations , PointOperations , PointStruct , QueryEnum , ScoredPoint , SearchRequest ,
UpdateOperation , ValueVariants , VectorInternal , Vectors , WithPayloadInterface , WithVector ,
2026-06-02 15:22:59 +00:00
} ;
use serde ::{ Deserialize , Serialize } ;
use tauri ::Manager ;
use crate ::api_token ::APIToken ;
use crate ::environment ::DATA_DIRECTORY ;
use crate ::metadata ::META_DATA ;
const VECTOR_NAME : & str = " embedding " ;
const HNSW_M : usize = 16 ;
const HNSW_EF_CONSTRUCT : usize = 100 ;
const HNSW_FULL_SCAN_THRESHOLD_KB : usize = 10_000 ;
const HNSW_MAX_INDEXING_THREADS : usize = 0 ;
const VECTOR_INDEXING_THRESHOLD_KB : usize = 10_000 ;
2026-09-09 16:43:37 +00:00
const STORE_INITIALIZATION_MARKER : & str = " store_name.txt " ;
const STORE_INITIALIZATION_MARKER_TEMP : & str = " store_name.tmp " ;
const STORE_DISPLAY_NAME_MARKER : & str = " data_source_name.txt " ;
const STORE_DISPLAY_NAME_MARKER_TEMP : & str = " data_source_name.tmp " ;
2026-06-02 15:22:59 +00:00
type QdrantEdgeResult < T > = Result < T , Box < dyn std ::error ::Error + Send + Sync > > ;
static QDRANT_EDGE_DATABASE : Lazy < Mutex < Option < QdrantEdgeDatabase > > > =
Lazy ::new ( | | Mutex ::new ( None ) ) ;
static QDRANT_EDGE_STATUS : Lazy < Mutex < QdrantEdgeStatusInfo > > =
Lazy ::new ( | | Mutex ::new ( QdrantEdgeStatusInfo ::default ( ) ) ) ;
#[ derive(Default) ]
struct QdrantEdgeStatusInfo {
status : QdrantEdgeStatus ,
unavailable_reason : Option < String > ,
}
#[ derive(Clone, Copy, Default, Serialize, PartialEq, Eq) ]
pub enum QdrantEdgeStatus {
#[ default ]
Starting ,
Available ,
Unavailable ,
}
#[ derive(Serialize) ]
pub struct QdrantEdgeServiceInfo {
pub status : QdrantEdgeStatus ,
pub name : String ,
pub version : String ,
pub path : String ,
pub stores_count : usize ,
pub is_available : bool ,
pub unavailable_reason : Option < String > ,
}
#[ derive(Clone, Deserialize) ]
pub struct QdrantEdgeStoragePoint {
pub point_id : String ,
pub vector : Vec < f32 > ,
pub data_source_id : String ,
pub data_source_type : String ,
2026-09-09 16:43:37 +00:00
pub chunk_id : String ,
pub parent_file_id : String ,
2026-06-02 15:22:59 +00:00
pub file_path : String ,
2026-09-09 16:43:37 +00:00
pub absolute_path : String ,
2026-06-02 15:22:59 +00:00
pub file_name : String ,
pub relative_path : String ,
2026-09-09 16:43:37 +00:00
pub file_type : String ,
pub page_number : Option < i32 > ,
2026-06-02 15:22:59 +00:00
pub chunk_index : i32 ,
pub text : String ,
pub fingerprint : String ,
2026-09-09 16:43:37 +00:00
pub creation_utc : String ,
2026-06-02 15:22:59 +00:00
pub last_write_utc : String ,
pub embedded_at_utc : String ,
}
#[ derive(Deserialize) ]
pub struct EnsureQdrantEdgeStoreRequest {
pub store_name : String ,
2026-09-09 16:43:37 +00:00
pub data_source_name : String ,
2026-06-02 15:22:59 +00:00
pub vector_size : usize ,
}
#[ derive(Deserialize) ]
pub struct InsertQdrantEdgeEmbeddingRequest {
pub store_name : String ,
pub points : Vec < QdrantEdgeStoragePoint > ,
}
2026-09-09 16:43:37 +00:00
#[ derive(Deserialize) ]
pub struct SearchQdrantEdgeEmbeddingRequest {
pub store_name : String ,
pub vector : Vec < f32 > ,
pub max_matches : usize ,
}
2026-06-02 15:22:59 +00:00
#[ derive(Deserialize) ]
pub struct DeleteQdrantEdgeEmbeddingByFileRequest {
pub store_name : String ,
pub file_path : String ,
}
2026-09-09 16:43:37 +00:00
#[ derive(Deserialize) ]
pub struct OptimizeQdrantEdgeStoreRequest {
pub store_name : String ,
}
2026-06-02 15:22:59 +00:00
#[ derive(Deserialize) ]
pub struct DeleteQdrantEdgeStoreRequest {
pub store_name : String ,
}
#[ derive(Serialize) ]
2026-09-09 16:43:37 +00:00
pub struct QdrantEdgeResponse < T > {
2026-06-02 15:22:59 +00:00
pub success : bool ,
pub issue : String ,
2026-09-09 16:43:37 +00:00
pub data : Option < T > ,
}
#[ derive(Serialize) ]
pub struct QdrantEdgeEnsureStoreResult {
pub created : bool ,
}
#[ derive(Serialize) ]
pub struct QdrantEdgeSearchResult {
pub point_id : String ,
pub score : f32 ,
pub data_source_id : String ,
pub data_source_type : String ,
pub chunk_id : String ,
pub parent_file_id : String ,
pub file_path : String ,
pub absolute_path : String ,
pub file_name : String ,
pub relative_path : String ,
pub file_type : String ,
pub page_number : Option < i32 > ,
pub chunk_index : i32 ,
pub text : String ,
pub fingerprint : String ,
pub creation_utc : String ,
pub last_write_utc : String ,
pub embedded_at_utc : String ,
2026-06-02 15:22:59 +00:00
}
#[ derive(Clone, Serialize) ]
pub struct QdrantEdgeInfo {
pub name : String ,
pub version : String ,
pub path : String ,
pub stores_count : usize ,
}
pub struct QdrantEdgeDatabase {
base_path : PathBuf ,
shards : HashMap < String , EdgeShard > ,
}
impl QdrantEdgeDatabase {
pub fn new ( base_path : PathBuf ) -> Self {
Self {
base_path ,
shards : HashMap ::new ( ) ,
}
}
fn store_path ( & self , store_name : & str ) -> QdrantEdgeResult < PathBuf > {
validate_store_name ( store_name ) ? ;
2026-09-09 16:43:37 +00:00
Ok ( self . base_path . join ( " stores " ) . join ( store_directory_name ( store_name ) ) )
2026-06-02 15:22:59 +00:00
}
// To ensure a shard exists and that you can insert a vector
2026-09-09 16:43:37 +00:00
fn get_or_create_store ( & mut self , store_name : & str , vector_size : usize ) -> QdrantEdgeResult < ( & EdgeShard , bool ) > {
let ( path , is_initialized ) = self . reconcile_store_state ( store_name ) ? ;
2026-06-02 15:22:59 +00:00
if self . shards . contains_key ( store_name ) {
2026-09-09 16:43:37 +00:00
return Ok ( ( self . shards . get ( store_name ) . unwrap ( ) , false ) ) ;
2026-06-02 15:22:59 +00:00
}
2026-09-09 16:43:37 +00:00
let shard = if is_initialized {
EdgeShard ::load ( & path , None ) . map_err ( | error | {
format! ( " Failed to load vector store ' {store_name} ' from ' {} ': {error} " , path . display ( ) )
} ) ?
2026-06-02 15:22:59 +00:00
} else {
2026-09-09 16:43:37 +00:00
fs ::create_dir_all ( & path ) . map_err ( | error | {
format! ( " Failed to create directory for vector store ' {store_name} ' at ' {} ': {error} " , path . display ( ) )
} ) ? ;
let shard = match EdgeShard ::new ( & path , edge_config ( vector_size ) ) {
Ok ( shard ) = > shard ,
Err ( error ) = > {
let cleanup_issue = remove_partial_store ( & path ) ;
return Err ( format! ( " Failed to create vector store ' {store_name} ' at ' {} ': {error} {cleanup_issue} " , path . display ( ) ) . into ( ) ) ;
} ,
} ;
if let Err ( error ) = write_store_initialization_marker ( & path , store_name ) {
drop ( shard ) ;
let cleanup_issue = remove_partial_store ( & path ) ;
return Err ( format! ( " Failed to finalize vector store ' {store_name} ' at ' {} ': {error} {cleanup_issue} " , path . display ( ) ) . into ( ) ) ;
}
shard
2026-06-02 15:22:59 +00:00
} ;
self . shards . insert ( store_name . to_string ( ) , shard ) ;
2026-09-09 16:43:37 +00:00
Ok ( ( self . shards . get ( store_name ) . unwrap ( ) , ! is_initialized ) )
2026-06-02 15:22:59 +00:00
}
// To check whether a shard exists so you can delete a file from it
fn get_existing_store ( & mut self , store_name : & str ) -> QdrantEdgeResult < Option < & EdgeShard > > {
2026-09-09 16:43:37 +00:00
let ( path , is_initialized ) = self . reconcile_store_state ( store_name ) ? ;
2026-06-02 15:22:59 +00:00
if self . shards . contains_key ( store_name ) {
return Ok ( self . shards . get ( store_name ) ) ;
}
2026-09-09 16:43:37 +00:00
if ! is_initialized {
2026-06-02 15:22:59 +00:00
return Ok ( None ) ;
}
2026-09-09 16:43:37 +00:00
let shard = EdgeShard ::load ( & path , None ) . map_err ( | error | {
format! ( " Failed to load vector store ' {store_name} ' from ' {} ': {error} " , path . display ( ) )
} ) ? ;
2026-06-02 15:22:59 +00:00
self . shards . insert ( store_name . to_string ( ) , shard ) ;
Ok ( self . shards . get ( store_name ) )
}
2026-09-09 16:43:37 +00:00
fn reconcile_store_state ( & mut self , store_name : & str ) -> QdrantEdgeResult < ( PathBuf , bool ) > {
let path = self . store_path ( store_name ) ? ;
let is_initialized = store_is_initialized ( & path , store_name ) ? ;
if self . shards . contains_key ( store_name ) & & ! is_initialized {
warn! ( Source = " Qdrant Edge " ; " Removing stale cached vector store '{}' because its initialized data directory no longer exists. " , store_name ) ;
self . shards . remove ( store_name ) ;
}
if path . exists ( ) & & ! is_initialized {
warn! ( Source = " Qdrant Edge " ; " Removing incompletely initialized vector store '{}' before continuing. " , store_name ) ;
fs ::remove_dir_all ( & path ) . map_err ( | error | {
format! ( " Failed to remove incomplete vector store ' {store_name} ' at ' {} ': {error} " , path . display ( ) )
} ) ? ;
}
Ok ( ( path , is_initialized ) )
}
2026-06-02 15:22:59 +00:00
fn info ( & self ) -> QdrantEdgeResult < QdrantEdgeInfo > {
let stores_path = self . base_path . join ( " stores " ) ;
let stores_count = if stores_path . exists ( ) {
fs ::read_dir ( stores_path ) ?
. filter_map ( Result ::ok )
2026-09-09 16:43:37 +00:00
. filter ( | entry | entry . path ( ) . join ( STORE_INITIALIZATION_MARKER ) . is_file ( ) )
2026-06-02 15:22:59 +00:00
. count ( )
} else {
0
} ;
Ok ( QdrantEdgeInfo {
name : " Qdrant Edge " . to_string ( ) ,
version : vector_store_version ( ) ? ,
path : self . base_path . to_string_lossy ( ) . to_string ( ) ,
stores_count ,
} )
}
2026-09-09 16:43:37 +00:00
fn ensure_store_exists ( & mut self , store_name : & str , data_source_name : & str , vector_size : usize ) -> QdrantEdgeResult < QdrantEdgeEnsureStoreResult > {
2026-06-02 15:22:59 +00:00
validate_vector_size ( vector_size ) ? ;
2026-09-09 16:43:37 +00:00
validate_data_source_name ( data_source_name ) ? ;
let store_path = self . store_path ( store_name ) ? ;
let ( _ , created ) = self . get_or_create_store ( store_name , vector_size ) ? ;
write_store_display_name ( & store_path , data_source_name ) ? ;
Ok ( QdrantEdgeEnsureStoreResult {
created ,
} )
2026-06-02 15:22:59 +00:00
}
fn insert_embedding ( & mut self , store_name : & str , points : Vec < QdrantEdgeStoragePoint > ) -> QdrantEdgeResult < ( ) > {
let Some ( first_point ) = points . first ( ) else {
return Ok ( ( ) ) ;
} ;
let vector_size = first_point . vector . len ( ) ;
validate_vector_size ( vector_size ) ? ;
if points . iter ( ) . any ( | point | point . vector . len ( ) ! = vector_size ) {
return Err ( " All vectors in one insert request must have the same size. " . into ( ) ) ;
}
2026-09-09 16:43:37 +00:00
let ( shard , _ ) = self . get_or_create_store ( store_name , vector_size ) ? ;
2026-06-02 15:22:59 +00:00
let points = points
. into_iter ( )
. map ( to_qdrant_edge_point )
2026-09-09 16:43:37 +00:00
. collect ::< QdrantEdgeResult < Vec < _ > > > ( ) ? ;
2026-06-02 15:22:59 +00:00
shard . update ( UpdateOperation ::PointOperation (
PointOperations ::UpsertPoints ( PointInsertOperations ::PointsList ( points ) ) ,
) ) ? ;
shard . flush ( ) ;
Ok ( ( ) )
}
2026-09-09 16:43:37 +00:00
fn search_embedding ( & mut self , store_name : & str , vector : Vec < f32 > , max_matches : usize ) -> QdrantEdgeResult < Vec < QdrantEdgeSearchResult > > {
if max_matches = = 0 {
return Ok ( vec! [ ] ) ;
}
validate_vector_size ( vector . len ( ) ) ? ;
let Some ( shard ) = self . get_existing_store ( store_name ) ? else {
return Ok ( vec! [ ] ) ;
} ;
let search_results = shard . search ( SearchRequest {
query : QueryEnum ::Nearest ( NamedQuery ::new (
VectorInternal ::Dense ( vector ) ,
VECTOR_NAME ,
) ) ,
filter : None ,
params : None ,
limit : max_matches ,
offset : 0 ,
with_payload : Some ( WithPayloadInterface ::Bool ( true ) ) ,
with_vector : Some ( WithVector ::Bool ( false ) ) ,
score_threshold : None ,
} ) ? ;
Ok ( search_results
. into_iter ( )
. map ( to_qdrant_edge_search_result )
. collect ( ) )
}
2026-06-02 15:22:59 +00:00
fn delete_embedding_by_file ( & mut self , store_name : & str , file_path : & str ) -> QdrantEdgeResult < ( ) > {
let Some ( shard ) = self . get_existing_store ( store_name ) ? else {
return Ok ( ( ) ) ;
} ;
shard . update ( UpdateOperation ::PointOperation (
PointOperations ::DeletePointsByFilter ( match_keyword_filter ( " file_path " , file_path ) ? ) ,
) ) ? ;
shard . flush ( ) ;
Ok ( ( ) )
}
2026-09-09 16:43:37 +00:00
fn optimize_store ( & mut self , store_name : & str ) -> QdrantEdgeResult < ( ) > {
let Some ( shard ) = self . get_existing_store ( store_name ) ? else {
return Ok ( ( ) ) ;
} ;
let optimized = shard . optimize ( ) ? ;
if optimized {
info! ( Source = " Qdrant Edge " ; " Optimized vector store '{}'. " , store_name ) ;
}
shard . flush ( ) ;
Ok ( ( ) )
}
2026-06-02 15:22:59 +00:00
fn delete_store ( & mut self , store_name : & str ) -> QdrantEdgeResult < ( ) > {
self . shards . remove ( store_name ) ;
let path = self . store_path ( store_name ) ? ;
if path . exists ( ) {
fs ::remove_dir_all ( path ) ? ;
}
Ok ( ( ) )
}
fn base_path ( & self ) -> PathBuf {
self . base_path . clone ( )
}
}
2026-09-09 16:43:37 +00:00
fn store_directory_name ( store_name : & str ) -> String {
let stable_id = store_name . strip_prefix ( " rag_ " ) . unwrap_or ( store_name ) ;
format! ( " store_ {stable_id} " )
}
2026-06-02 15:22:59 +00:00
fn qdrant_edge_base_path ( ) -> QdrantEdgeResult < PathBuf > {
let data_directory = DATA_DIRECTORY
. get ( )
. ok_or ( " The data directory has not been initialized. " ) ? ;
Ok ( Path ::new ( data_directory )
. join ( " databases " )
. join ( " vector_database " ) )
}
pub async fn qdrant_edge_info ( _token : APIToken ) -> Json < QdrantEdgeServiceInfo > {
let status = QDRANT_EDGE_STATUS . lock ( ) . unwrap ( ) ;
let current_status = status . status ;
let unavailable_reason = status . unavailable_reason . clone ( ) ;
drop ( status ) ;
let database_guard = QDRANT_EDGE_DATABASE . lock ( ) . unwrap ( ) ;
let database_info = database_guard
. as_ref ( )
. and_then ( | database | database . info ( ) . ok ( ) ) ;
let is_available = current_status = = QdrantEdgeStatus ::Available & & database_info . is_some ( ) ;
Json ( QdrantEdgeServiceInfo {
status : current_status ,
name : database_info . as_ref ( ) . map ( | info | info . name . clone ( ) ) . unwrap_or_default ( ) ,
version : database_info . as_ref ( ) . map ( | info | info . version . clone ( ) ) . unwrap_or_default ( ) ,
path : database_info . as_ref ( ) . map ( | info | info . path . clone ( ) ) . unwrap_or_default ( ) ,
stores_count : database_info . as_ref ( ) . map ( | info | info . stores_count ) . unwrap_or_default ( ) ,
is_available ,
unavailable_reason ,
} )
}
2026-09-09 16:43:37 +00:00
pub async fn ensure_qdrant_edge_store ( _token : APIToken , Json ( request ) : Json < EnsureQdrantEdgeStoreRequest > ) -> Json < QdrantEdgeResponse < QdrantEdgeEnsureStoreResult > > {
execute_qdrant_edge_request ( | database | {
database . ensure_store_exists ( & request . store_name , & request . data_source_name , request . vector_size )
2026-06-02 15:22:59 +00:00
} )
}
2026-09-09 16:43:37 +00:00
pub async fn insert_qdrant_edge_embedding ( _token : APIToken , Json ( request ) : Json < InsertQdrantEdgeEmbeddingRequest > ) -> Json < QdrantEdgeResponse < ( ) > > {
execute_qdrant_edge_request ( | database | {
2026-06-02 15:22:59 +00:00
database . insert_embedding ( & request . store_name , request . points )
} )
}
2026-09-09 16:43:37 +00:00
pub async fn search_qdrant_edge_embeddings ( _token : APIToken , Json ( request ) : Json < SearchQdrantEdgeEmbeddingRequest > ) -> Json < QdrantEdgeResponse < Vec < QdrantEdgeSearchResult > > > {
execute_qdrant_edge_request ( | database | {
database . search_embedding ( & request . store_name , request . vector , request . max_matches )
} )
}
pub async fn delete_qdrant_edge_embedding_by_file ( _token : APIToken , Json ( request ) : Json < DeleteQdrantEdgeEmbeddingByFileRequest > ) -> Json < QdrantEdgeResponse < ( ) > > {
execute_qdrant_edge_request ( | database | {
2026-06-02 15:22:59 +00:00
database . delete_embedding_by_file ( & request . store_name , & request . file_path )
} )
}
2026-09-09 16:43:37 +00:00
pub async fn optimize_qdrant_edge_store ( _token : APIToken , Json ( request ) : Json < OptimizeQdrantEdgeStoreRequest > ) -> Json < QdrantEdgeResponse < ( ) > > {
execute_qdrant_edge_request ( | database | {
database . optimize_store ( & request . store_name )
} )
}
pub async fn delete_qdrant_edge_store ( _token : APIToken , Json ( request ) : Json < DeleteQdrantEdgeStoreRequest > ) -> Json < QdrantEdgeResponse < ( ) > > {
execute_qdrant_edge_request ( | database | {
2026-06-02 15:22:59 +00:00
database . delete_store ( & request . store_name )
} )
}
pub fn start_qdrant_edge_database < R : tauri ::Runtime > ( app_handle : tauri ::AppHandle < R > ) {
set_qdrant_edge_starting ( ) ;
remove_obsolete_qdrant_sidecar_files ( & app_handle ) ;
let path = match qdrant_edge_base_path ( ) {
Ok ( path ) = > path ,
Err ( e ) = > {
let reason = format! ( " Qdrant Edge cannot be started: {e} " ) ;
error! ( Source = " Qdrant Edge " ; " {reason} " ) ;
set_qdrant_edge_unavailable ( reason ) ;
return ;
} ,
} ;
match fs ::create_dir_all ( & path ) {
Ok ( _ ) = > {
let database = QdrantEdgeDatabase ::new ( path . clone ( ) ) ;
* QDRANT_EDGE_DATABASE . lock ( ) . unwrap ( ) = Some ( database ) ;
set_qdrant_edge_available ( ) ;
info! ( Source = " Qdrant Edge " ; " Qdrant Edge is available at '{}'. " , path . display ( ) ) ;
} ,
Err ( e ) = > {
let reason = format! ( " The Qdrant Edge data directory could not be created: {e} " ) ;
error! ( Source = " Qdrant Edge " ; " {reason} " ) ;
set_qdrant_edge_unavailable ( reason ) ;
} ,
}
}
pub fn stop_qdrant_edge_database ( ) {
if let Some ( database ) = QDRANT_EDGE_DATABASE . lock ( ) . unwrap ( ) . take ( ) {
info! ( Source = " Qdrant Edge " ; " Stopping Qdrant Edge at '{}'. " , database . base_path ( ) . display ( ) ) ;
drop ( database ) ;
}
set_qdrant_edge_unavailable ( " Qdrant Edge was stopped. " . to_string ( ) ) ;
}
2026-09-09 16:43:37 +00:00
fn execute_qdrant_edge_request < T , F > ( operation : F ) -> Json < QdrantEdgeResponse < T > >
2026-06-02 15:22:59 +00:00
where
2026-09-09 16:43:37 +00:00
T : Serialize ,
F : FnOnce ( & mut QdrantEdgeDatabase ) -> QdrantEdgeResult < T > ,
2026-06-02 15:22:59 +00:00
{
let mut database_guard = QDRANT_EDGE_DATABASE . lock ( ) . unwrap ( ) ;
let Some ( database ) = database_guard . as_mut ( ) else {
2026-09-09 16:43:37 +00:00
return Json ( QdrantEdgeResponse {
2026-06-02 15:22:59 +00:00
success : false ,
issue : " Qdrant Edge is not available. " . to_string ( ) ,
2026-09-09 16:43:37 +00:00
data : None ,
2026-06-02 15:22:59 +00:00
} ) ;
} ;
match operation ( database ) {
2026-09-09 16:43:37 +00:00
Ok ( data ) = > Json ( QdrantEdgeResponse {
2026-06-02 15:22:59 +00:00
success : true ,
issue : String ::new ( ) ,
2026-09-09 16:43:37 +00:00
data : Some ( data ) ,
2026-06-02 15:22:59 +00:00
} ) ,
Err ( e ) = > {
let issue = e . to_string ( ) ;
2026-09-09 16:43:37 +00:00
error! ( Source = " Qdrant Edge " ; " Qdrant Edge request failed: {issue} " ) ;
Json ( QdrantEdgeResponse {
2026-06-02 15:22:59 +00:00
success : false ,
issue ,
2026-09-09 16:43:37 +00:00
data : None ,
2026-06-02 15:22:59 +00:00
} )
} ,
}
}
fn set_qdrant_edge_available ( ) {
let mut status = QDRANT_EDGE_STATUS . lock ( ) . unwrap ( ) ;
status . status = QdrantEdgeStatus ::Available ;
status . unavailable_reason = None ;
}
fn set_qdrant_edge_starting ( ) {
let mut status = QDRANT_EDGE_STATUS . lock ( ) . unwrap ( ) ;
status . status = QdrantEdgeStatus ::Starting ;
status . unavailable_reason = None ;
}
fn set_qdrant_edge_unavailable ( reason : String ) {
let mut status = QDRANT_EDGE_STATUS . lock ( ) . unwrap ( ) ;
status . status = QdrantEdgeStatus ::Unavailable ;
status . unavailable_reason = Some ( reason ) ;
}
2026-06-20 14:11:24 +00:00
// Temporary compatibility shim until 2026-12-02:
// documentation/compatibility-shims/2026-06-qdrant-edge-migration.md
2026-06-02 15:22:59 +00:00
fn remove_obsolete_qdrant_sidecar_files < R : tauri ::Runtime > ( app_handle : & tauri ::AppHandle < R > ) {
let mut paths = Vec ::new ( ) ;
if let Some ( data_directory ) = DATA_DIRECTORY . get ( ) {
let databases_directory = Path ::new ( data_directory ) . join ( " databases " ) ;
paths . push ( databases_directory . join ( " qdrant " ) ) ;
paths . push ( databases_directory . join ( " qdrant_test " ) ) ;
}
if let Ok ( resource_dir ) = app_handle . path ( ) . resource_dir ( ) {
paths . push ( resource_dir . join ( " target " ) . join ( " databases " ) . join ( " qdrant " ) ) ;
paths . push ( resource_dir . join ( " resources " ) . join ( " databases " ) . join ( " qdrant " ) ) ;
}
cfg_if ::cfg_if! {
if #[ cfg(any(target_os = " windows " , target_os = " macos " )) ] {
2026-07-04 12:07:32 +00:00
if let Ok ( current_exe ) = std ::env ::current_exe ( ) & & let Some ( exe_dir ) = current_exe . parent ( )
& & exe_dir . to_string_lossy ( ) . contains ( " MindWork AI Studio " ) {
paths . push ( exe_dir . join ( " target " ) . join ( " databases " ) . join ( " qdrant " ) ) ;
paths . push ( exe_dir . join ( " qdrant.exe " ) ) ;
paths . push ( exe_dir . join ( " qdrant " ) ) ;
2026-06-02 15:22:59 +00:00
}
}
}
for path in paths {
remove_obsolete_qdrant_path ( & path ) ;
}
}
fn remove_obsolete_qdrant_path ( path : & Path ) {
if ! path . exists ( ) {
info! ( Source = " Qdrant Edge " ; " Obsolete file or directory '{}' was not found. " , path . display ( ) ) ;
return ;
}
let result = if path . is_dir ( ) {
fs ::remove_dir_all ( path )
} else {
fs ::remove_file ( path )
} ;
match result {
Ok ( _ ) = > warn! ( Source = " Qdrant Edge " ; " Removed obsolete Qdrant sidecar file or directory '{}'. " , path . display ( ) ) ,
Err ( e ) = > warn! ( Source = " Qdrant Edge " ; " Could not remove obsolete Qdrant sidecar file or directory '{}': {e} " , path . display ( ) ) ,
}
}
fn edge_config ( vector_size : usize ) -> EdgeConfig {
EdgeConfig {
on_disk_payload : true ,
vectors : HashMap ::from ( [ (
VECTOR_NAME . to_string ( ) ,
EdgeVectorParams {
size : vector_size ,
distance : Distance ::Cosine ,
on_disk : Some ( true ) ,
quantization_config : None ,
multivector_config : None ,
datatype : None ,
hnsw_config : Some ( hnsw_config ( ) ) ,
} ,
) ] ) ,
sparse_vectors : HashMap ::new ( ) ,
hnsw_config : hnsw_config ( ) ,
quantization_config : None ,
optimizers : edge_optimizers_config ( ) ,
2026-06-04 13:28:33 +00:00
wal_options : None ,
2026-06-02 15:22:59 +00:00
}
}
fn hnsw_config ( ) -> HnswIndexConfig {
HnswIndexConfig {
m : HNSW_M ,
ef_construct : HNSW_EF_CONSTRUCT ,
full_scan_threshold : HNSW_FULL_SCAN_THRESHOLD_KB ,
max_indexing_threads : HNSW_MAX_INDEXING_THREADS ,
on_disk : Some ( true ) ,
payload_m : None ,
inline_storage : None ,
}
}
fn edge_optimizers_config ( ) -> EdgeOptimizersConfig {
EdgeOptimizersConfig {
indexing_threshold : Some ( VECTOR_INDEXING_THRESHOLD_KB ) ,
prevent_unoptimized : Some ( false ) ,
.. Default ::default ( )
}
}
2026-09-09 16:43:37 +00:00
fn store_is_initialized ( path : & Path , store_name : & str ) -> QdrantEdgeResult < bool > {
if ! path . join ( " edge_config.json " ) . is_file ( ) | | ! path . join ( " segments " ) . is_dir ( ) {
return Ok ( false ) ;
}
let marker_path = path . join ( STORE_INITIALIZATION_MARKER ) ;
if ! marker_path . exists ( ) {
return Ok ( false ) ;
}
let initialized_store_name = fs ::read_to_string ( & marker_path ) . map_err ( | error | {
format! ( " Failed to read vector store initialization marker ' {} ': {error} " , marker_path . display ( ) )
} ) ? ;
if initialized_store_name ! = store_name {
return Err ( format! ( " Vector store path collision at ' {} ': expected store ' {} ', but the path belongs to ' {} '. " , path . display ( ) , store_name , initialized_store_name ) . into ( ) ) ;
}
Ok ( true )
}
fn write_store_initialization_marker ( path : & Path , store_name : & str ) -> std ::io ::Result < ( ) > {
write_store_marker ( path , STORE_INITIALIZATION_MARKER , STORE_INITIALIZATION_MARKER_TEMP , store_name )
}
fn write_store_display_name ( path : & Path , data_source_name : & str ) -> std ::io ::Result < ( ) > {
write_store_marker ( path , STORE_DISPLAY_NAME_MARKER , STORE_DISPLAY_NAME_MARKER_TEMP , data_source_name )
}
fn write_store_marker ( path : & Path , marker_name : & str , temporary_marker_name : & str , value : & str ) -> std ::io ::Result < ( ) > {
let marker_path = path . join ( marker_name ) ;
if fs ::read_to_string ( & marker_path ) . is_ok_and ( | current_value | current_value = = value ) {
return Ok ( ( ) ) ;
}
let temporary_marker_path = path . join ( temporary_marker_name ) ;
fs ::write ( & temporary_marker_path , value ) ? ;
if marker_path . exists ( ) {
fs ::remove_file ( & marker_path ) ? ;
}
fs ::rename ( temporary_marker_path , marker_path )
}
fn remove_partial_store ( path : & Path ) -> String {
match fs ::remove_dir_all ( path ) {
Ok ( ( ) ) = > String ::new ( ) ,
Err ( error ) = > format! ( " The incomplete store could not be removed: {error} " ) ,
}
2026-06-02 15:22:59 +00:00
}
fn validate_vector_size ( vector_size : usize ) -> QdrantEdgeResult < ( ) > {
if vector_size = = 0 {
return Err ( " Vector size must be greater than zero. " . into ( ) ) ;
}
Ok ( ( ) )
}
2026-09-09 16:43:37 +00:00
fn validate_data_source_name ( data_source_name : & str ) -> QdrantEdgeResult < ( ) > {
const MAX_DATA_SOURCE_NAME_LENGTH : usize = 40 ;
if data_source_name . trim ( ) . is_empty ( ) {
return Err ( " Data source name cannot be empty. " . into ( ) ) ;
}
if data_source_name . chars ( ) . count ( ) > MAX_DATA_SOURCE_NAME_LENGTH {
return Err ( format! ( " Data source name exceeds the maximum length of {MAX_DATA_SOURCE_NAME_LENGTH} characters. " ) . into ( ) ) ;
}
if data_source_name . chars ( ) . any ( | c | c . is_control ( ) ) {
return Err ( " Data source name contains unsupported control characters. " . into ( ) ) ;
}
Ok ( ( ) )
}
2026-06-02 15:22:59 +00:00
fn vector_store_version ( ) -> QdrantEdgeResult < String > {
let metadata = META_DATA
. lock ( )
. map_err ( | _ | " Metadata lock was poisoned. " ) ? ;
let Some ( metadata ) = metadata . as_ref ( ) else {
return Err ( " Metadata was not initialized. " . into ( ) ) ;
} ;
Ok ( metadata . vector_store_version . clone ( ) )
}
2026-09-09 16:43:37 +00:00
fn to_qdrant_edge_point ( point : QdrantEdgeStoragePoint ) -> QdrantEdgeResult < qdrant_edge ::PointStructPersisted > {
Ok ( PointStruct ::new (
to_point_id ( & point . point_id ) ? ,
2026-06-02 15:22:59 +00:00
Vectors ::new_named ( [ ( VECTOR_NAME , point . vector ) ] ) ,
json! ( {
" data_source_id " : point . data_source_id ,
" data_source_type " : point . data_source_type ,
2026-09-09 16:43:37 +00:00
" chunk_id " : point . chunk_id ,
" parent_file_id " : point . parent_file_id ,
2026-06-02 15:22:59 +00:00
" file_path " : point . file_path ,
2026-09-09 16:43:37 +00:00
" absolute_path " : point . absolute_path ,
2026-06-02 15:22:59 +00:00
" file_name " : point . file_name ,
" relative_path " : point . relative_path ,
2026-09-09 16:43:37 +00:00
" file_type " : point . file_type ,
" page_number " : point . page_number ,
2026-06-02 15:22:59 +00:00
" chunk_index " : point . chunk_index ,
" text " : point . text ,
" fingerprint " : point . fingerprint ,
2026-09-09 16:43:37 +00:00
" creation_utc " : point . creation_utc ,
2026-06-02 15:22:59 +00:00
" last_write_utc " : point . last_write_utc ,
" embedded_at_utc " : point . embedded_at_utc ,
} ) ,
)
2026-09-09 16:43:37 +00:00
. into ( ) )
}
fn to_qdrant_edge_search_result ( point : ScoredPoint ) -> QdrantEdgeSearchResult {
let payload = point . payload . unwrap_or_default ( ) ;
QdrantEdgeSearchResult {
point_id : point_id_to_string ( point . id ) ,
score : point . score ,
data_source_id : payload_string ( & payload , " data_source_id " ) ,
data_source_type : payload_string ( & payload , " data_source_type " ) ,
chunk_id : payload_string ( & payload , " chunk_id " ) ,
parent_file_id : payload_string ( & payload , " parent_file_id " ) ,
file_path : payload_string ( & payload , " file_path " ) ,
absolute_path : payload_string ( & payload , " absolute_path " ) ,
file_name : payload_string ( & payload , " file_name " ) ,
relative_path : payload_string ( & payload , " relative_path " ) ,
file_type : payload_string ( & payload , " file_type " ) ,
page_number : payload_i32 ( & payload , " page_number " ) ,
chunk_index : payload_i32 ( & payload , " chunk_index " ) . unwrap_or_default ( ) ,
text : payload_string ( & payload , " text " ) ,
fingerprint : payload_string ( & payload , " fingerprint " ) ,
creation_utc : payload_string ( & payload , " creation_utc " ) ,
last_write_utc : payload_string ( & payload , " last_write_utc " ) ,
embedded_at_utc : payload_string ( & payload , " embedded_at_utc " ) ,
}
2026-06-02 15:22:59 +00:00
}
2026-09-09 16:43:37 +00:00
fn to_point_id ( point_id : & str ) -> QdrantEdgeResult < PointId > {
2026-06-02 15:22:59 +00:00
Uuid ::parse_str ( point_id )
. map ( PointId ::Uuid )
2026-09-09 16:43:37 +00:00
. map_err ( | _ | " Vector point ID must be a valid UUID. " . into ( ) )
2026-06-02 15:22:59 +00:00
}
2026-09-09 16:43:37 +00:00
fn point_id_to_string ( point_id : PointId ) -> String {
match point_id {
PointId ::NumId ( id ) = > id . to_string ( ) ,
PointId ::Uuid ( uuid ) = > uuid . to_string ( ) ,
2026-06-02 15:22:59 +00:00
}
2026-09-09 16:43:37 +00:00
}
fn payload_string ( payload : & Payload , key : & str ) -> String {
payload
. 0
. get ( key )
. and_then ( Value ::as_str )
. unwrap_or_default ( )
. to_string ( )
}
2026-06-02 15:22:59 +00:00
2026-09-09 16:43:37 +00:00
fn payload_i32 ( payload : & Payload , key : & str ) -> Option < i32 > {
payload
. 0
. get ( key )
. and_then ( Value ::as_i64 )
. and_then ( | value | i32 ::try_from ( value ) . ok ( ) )
2026-06-02 15:22:59 +00:00
}
fn match_keyword_filter ( field_name : & str , value : & str ) -> QdrantEdgeResult < Filter > {
Ok ( Filter {
should : None ,
min_should : None ,
must : Some ( vec! [ Condition ::Field ( FieldCondition ::new_match (
field_name
. try_into ( )
. map_err ( | _ | format! ( " Invalid payload field name ' {field_name} '. " ) ) ? ,
Match ::Value ( MatchValue {
value : ValueVariants ::String ( value . to_string ( ) ) ,
} ) ,
) ) ] ) ,
must_not : None ,
} )
}
fn validate_store_name ( store_name : & str ) -> QdrantEdgeResult < ( ) > {
2026-09-09 16:43:37 +00:00
const MAX_STORE_NAME_LENGTH : usize = 128 ;
2026-06-02 15:22:59 +00:00
if store_name . is_empty ( ) {
return Err ( " Vector store name cannot be empty. " . into ( ) ) ;
}
2026-09-09 16:43:37 +00:00
if store_name . len ( ) > MAX_STORE_NAME_LENGTH {
return Err ( format! ( " Vector store name exceeds the maximum length of {MAX_STORE_NAME_LENGTH} bytes. " ) . into ( ) ) ;
2026-06-02 15:22:59 +00:00
}
if store_name
. chars ( )
2026-09-09 16:43:37 +00:00
. all ( | c | c . is_ascii_alphanumeric ( ) | | c = = '_' | | c = = '-' )
2026-06-02 15:22:59 +00:00
{
return Ok ( ( ) ) ;
}
Err ( format! ( " Vector store name ' {store_name} ' contains unsupported characters. " ) . into ( ) )
}
#[ cfg(test) ]
mod tests {
use super ::* ;
#[ test ]
fn validate_store_name_allows_safe_store_names ( ) {
2026-09-09 16:43:37 +00:00
assert! ( validate_store_name ( " rag_1234-abcd " ) . is_ok ( ) ) ;
2026-06-02 15:22:59 +00:00
}
#[ test ]
2026-09-09 16:43:37 +00:00
fn validate_store_name_rejects_path_syntax ( ) {
2026-06-02 15:22:59 +00:00
assert! ( validate_store_name ( " . " ) . is_err ( ) ) ;
assert! ( validate_store_name ( " .. " ) . is_err ( ) ) ;
2026-09-09 16:43:37 +00:00
assert! ( validate_store_name ( " ../store " ) . is_err ( ) ) ;
assert! ( validate_store_name ( " store \\ name " ) . is_err ( ) ) ;
}
#[ test ]
fn validate_store_name_rejects_oversized_names ( ) {
assert! ( validate_store_name ( & " a " . repeat ( 129 ) ) . is_err ( ) ) ;
}
#[ test ]
fn store_directory_name_contains_the_stable_data_source_id ( ) {
assert_eq! (
store_directory_name ( " rag_6cc665a82b1e4d42bc748015b7b391ec " ) ,
" store_6cc665a82b1e4d42bc748015b7b391ec "
) ;
}
#[ test ]
fn validate_data_source_name_allows_display_names_but_rejects_invalid_values ( ) {
assert! ( validate_data_source_name ( " Mäßig Confidence C# " ) . is_ok ( ) ) ;
assert! ( validate_data_source_name ( " " ) . is_err ( ) ) ;
assert! ( validate_data_source_name ( " invalid \n name " ) . is_err ( ) ) ;
assert! ( validate_data_source_name ( & " a " . repeat ( 41 ) ) . is_err ( ) ) ;
}
#[ test ]
fn ensure_store_reports_creation_and_updates_the_display_name ( ) {
let test_directory = std ::env ::temp_dir ( ) . join ( format! (
" ai-studio-qdrant-ensure-{}-{} " ,
std ::process ::id ( ) ,
std ::time ::SystemTime ::now ( )
. duration_since ( std ::time ::UNIX_EPOCH )
. unwrap ( )
. as_nanos ( )
) ) ;
let store_name = " rag_6cc665a82b1e4d42bc748015b7b391ec " ;
let mut database = QdrantEdgeDatabase ::new ( test_directory . clone ( ) ) ;
let created = database . ensure_store_exists ( store_name , " Original name " , 3 ) . unwrap ( ) ;
assert! ( created . created ) ;
let existing = database . ensure_store_exists ( store_name , " Renamed source " , 3 ) . unwrap ( ) ;
assert! ( ! existing . created ) ;
let display_name_path = database . store_path ( store_name ) . unwrap ( ) . join ( STORE_DISPLAY_NAME_MARKER ) ;
assert_eq! ( fs ::read_to_string ( display_name_path ) . unwrap ( ) , " Renamed source " ) ;
drop ( database ) ;
fs ::remove_dir_all ( test_directory ) . unwrap ( ) ;
}
#[ test ]
fn point_ids_must_be_valid_uuids ( ) {
assert! ( to_point_id ( " 6cc665a8-2b1e-4d42-bc74-8015b7b391ec " ) . is_ok ( ) ) ;
assert! ( to_point_id ( " deliberate-collision-input " ) . is_err ( ) ) ;
2026-06-02 15:22:59 +00:00
}
}