use crate::graph::embedding_inventory::EmbeddingEntity;
use crate::graph::embeddings::{store_name, text_column_of};
use crate::graph::mutation::validation::did_you_mean;
use crate::graph::schema::DirGraph;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Surface {
Method,
Cypher,
}
fn procedure_family(entity: EmbeddingEntity) -> &'static str {
match entity {
EmbeddingEntity::Node => "db.node_embeddings",
EmbeddingEntity::Relationship => "db.relationship_embeddings",
}
}
fn procedure_call(entity: EmbeddingEntity, name: &str, ty: &str, col: &str) -> String {
format!(
"CALL {}.{name}({{type: '{ty}', text_column: '{col}'}})",
procedure_family(entity)
)
}
impl Surface {
pub(crate) fn build_index(self, entity: EmbeddingEntity, ty: &str, col: &str) -> String {
match (self, entity) {
(Self::Method, EmbeddingEntity::Node) => format!("build_vector_index('{ty}', '{col}')"),
(Self::Method, EmbeddingEntity::Relationship) => {
format!("build_relationship_vector_index('{ty}', '{col}')")
}
(Self::Cypher, _) => procedure_call(entity, "build_index", ty, col),
}
}
pub(crate) fn write_store(self, entity: EmbeddingEntity, ty: &str, col: &str) -> String {
match (self, entity) {
(Self::Method, EmbeddingEntity::Node) => {
format!("embed_texts('{ty}', '{col}') or set_embeddings()")
}
(Self::Method, EmbeddingEntity::Relationship) => {
format!(
"embed_relationship_texts('{ty}', '{col}') or set_relationship_embeddings()"
)
}
(Self::Cypher, _) => format!(
"CALL {family}.embed or {family}.set with {{type: '{ty}', text_column: '{col}'}}",
family = procedure_family(entity)
),
}
}
fn reach(self, entity: EmbeddingEntity) -> String {
match self {
Self::Method => format!(
"pass entity='{}' (or call the {}_* method)",
entity.as_str(),
entity.as_str()
),
Self::Cypher => format!("use {}.*", procedure_family(entity)),
}
}
}
fn other(entity: EmbeddingEntity) -> EmbeddingEntity {
match entity {
EmbeddingEntity::Node => EmbeddingEntity::Relationship,
EmbeddingEntity::Relationship => EmbeddingEntity::Node,
}
}
fn stores(graph: &DirGraph, entity: EmbeddingEntity) -> Vec<(&str, &str)> {
let mut pairs: Vec<(&str, &str)> = match entity {
EmbeddingEntity::Node => graph
.embeddings
.keys()
.map(|(ty, name)| (ty.as_str(), text_column_of(name).unwrap_or(name)))
.collect(),
EmbeddingEntity::Relationship => graph
.edge_embeddings
.keys()
.map(|(ty, name)| (ty.as_str(), text_column_of(name).unwrap_or(name)))
.collect(),
};
pairs.sort_unstable();
pairs.dedup();
pairs
}
fn has_store(graph: &DirGraph, entity: EmbeddingEntity, ty: &str, col: &str) -> bool {
stores(graph, entity).contains(&(ty, col))
}
const LISTED_STORES: usize = 12;
pub(crate) fn missing_store_hint(
graph: &DirGraph,
entity: EmbeddingEntity,
ty: &str,
col: &str,
surface: Surface,
) -> String {
let known = stores(graph, entity);
if let Some(stripped) = text_column_of(col) {
if known.contains(&(ty, stripped)) {
return format!(
" Did you mean '{stripped}'? The text column is '{stripped}'; '{col}' is the \
embedding store's own name."
);
}
}
let other_entity = other(entity);
if has_store(graph, other_entity, ty, col) {
return format!(
" '{ty}.{col}' is a {} embedding store — {}.",
other_entity.as_str(),
surface.reach(other_entity)
);
}
let columns: Vec<&str> = known
.iter()
.filter(|(stored, _)| *stored == ty)
.map(|(_, column)| *column)
.collect();
let suggestion = did_you_mean(col, &columns);
if !suggestion.is_empty() {
return suggestion;
}
if columns.is_empty() {
let types: Vec<&str> = known
.iter()
.filter(|(_, column)| *column == col)
.map(|(stored, _)| *stored)
.collect();
let suggestion = did_you_mean(ty, &types);
if !suggestion.is_empty() {
return suggestion;
}
} else {
return format!(
" {} embedding stores of '{ty}': {}.",
capitalised(entity),
columns.join(", ")
);
}
if known.is_empty() {
return String::new();
}
let listed: Vec<String> = known
.iter()
.take(LISTED_STORES)
.map(|(stored, column)| format!("{stored}.{column}"))
.collect();
let more = known.len().saturating_sub(LISTED_STORES);
format!(
" {} embedding stores: {}{}.",
capitalised(entity),
listed.join(", "),
if more > 0 {
format!(" and {more} more")
} else {
String::new()
}
)
}
pub(crate) fn missing_column_hint(
graph: &DirGraph,
entity: EmbeddingEntity,
col: &str,
surface: Surface,
) -> String {
let known = stores(graph, entity);
if let Some(stripped) = text_column_of(col) {
if known.iter().any(|(_, column)| *column == stripped) {
return format!(
" Did you mean '{stripped}'? The text column is '{stripped}'; '{col}' is the \
embedding store's own name."
);
}
}
let other_entity = other(entity);
let elsewhere: Vec<&str> = stores(graph, other_entity)
.into_iter()
.filter(|(_, column)| *column == col)
.map(|(ty, _)| ty)
.collect();
if !elsewhere.is_empty() {
return format!(
" '{col}' is a {} embedding store (on {}) — {}.",
other_entity.as_str(),
elsewhere.join(", "),
surface.reach(other_entity)
);
}
let mut columns: Vec<&str> = known.iter().map(|(_, column)| *column).collect();
columns.sort_unstable();
columns.dedup();
let suggestion = did_you_mean(col, &columns);
if !suggestion.is_empty() {
return suggestion;
}
if columns.is_empty() {
String::new()
} else {
format!(
" {} embedding text columns: {}.",
capitalised(entity),
columns.join(", ")
)
}
}
fn capitalised(entity: EmbeddingEntity) -> &'static str {
match entity {
EmbeddingEntity::Node => "Node",
EmbeddingEntity::Relationship => "Relationship",
}
}
pub(crate) fn missing_store_error(
graph: &DirGraph,
entity: EmbeddingEntity,
ty: &str,
col: &str,
surface: Surface,
) -> String {
format!(
"No {} embedding store '{ty}.{col}'.{}",
entity.as_str(),
missing_store_hint(graph, entity, ty, col, surface)
)
}
pub(crate) fn no_index_to_refresh(
entity: EmbeddingEntity,
ty: &str,
col: &str,
surface: Surface,
) -> String {
let (store, deleted) = match entity {
EmbeddingEntity::Node => ("", "a delete of an embedded node"),
EmbeddingEntity::Relationship => (
"relationship store ",
"a delete of an embedded relationship or an endpoint",
),
};
format!(
"no vector index on {store}'{ty}.{}' to refresh — none was built, or {deleted} (or a \
vacuum()) dropped it. Build one with {}.",
store_name(col),
surface.build_index(entity, ty, col)
)
}
#[cfg(test)]
#[path = "embedding_hints_tests.rs"]
mod tests;