use std::collections::{BTreeSet, HashMap};
use petgraph::graph::{EdgeIndex, NodeIndex};
use rustc_hash::FxHashMap;
use super::carry::{
describe_group, group_keys, group_members, key_value, RelationshipEmbedding, RelationshipKeys,
};
use super::{
describe_relationship, replace_edge_embeddings_listed, require_carried_text_property,
upsert_edge_embeddings_listed,
};
use crate::datatypes::values::Value;
use crate::graph::algorithms::Interrupt;
use crate::graph::edge_embedding_generation::{
generate_selected, EdgeGenerationRequest, EmbeddingExecutionService, SelectedEdgeText,
};
use crate::graph::embedder::Embedder;
use crate::graph::embeddings::{EmbedError, EmbedHooks, EmbedMode, EmbedOutcome};
use crate::graph::schema::{DirGraph, InternedKey};
use crate::graph::storage::GraphRead;
#[derive(Debug, Clone, PartialEq)]
pub struct RelationshipVector {
pub source_type: Option<String>,
pub source_id: Value,
pub target_type: Option<String>,
pub target_id: Value,
pub key: Option<Value>,
pub vector: Vec<f32>,
}
impl From<RelationshipEmbedding> for RelationshipVector {
fn from(row: RelationshipEmbedding) -> Self {
RelationshipVector {
source_type: Some(row.source_type),
source_id: row.source_id,
target_type: Some(row.target_type),
target_id: row.target_id,
key: row.key,
vector: row.vector,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct RelationshipIngestReport {
pub stored: usize,
pub dimension: usize,
pub changed: usize,
pub store_created: bool,
}
pub fn set_relationship_embeddings<I>(
graph: &mut DirGraph,
relationship_type: &str,
text_column: &str,
rows: I,
keys: &RelationshipKeys,
metric: Option<&str>,
) -> Result<RelationshipIngestReport, String>
where
I: IntoIterator<Item = RelationshipVector>,
{
let Some(entries) = resolve_batch(graph, relationship_type, text_column, rows, keys)? else {
return Ok(RelationshipIngestReport::default());
};
replace_edge_embeddings_listed(
graph,
relationship_type,
text_column,
entries,
metric,
"rows",
)
.map(RelationshipIngestReport::from)
}
pub fn add_relationship_embeddings<I>(
graph: &mut DirGraph,
relationship_type: &str,
text_column: &str,
rows: I,
keys: &RelationshipKeys,
metric: Option<&str>,
) -> Result<RelationshipIngestReport, String>
where
I: IntoIterator<Item = RelationshipVector>,
{
let Some(entries) = resolve_batch(graph, relationship_type, text_column, rows, keys)? else {
return Ok(RelationshipIngestReport::default());
};
upsert_edge_embeddings_listed(
graph,
relationship_type,
text_column,
entries,
metric,
"rows",
)
.map(RelationshipIngestReport::from)
}
type ResolvedRow = (EdgeIndex, Vec<f32>);
fn resolve_batch<I>(
graph: &mut DirGraph,
relationship_type: &str,
text_column: &str,
rows: I,
keys: &RelationshipKeys,
) -> Result<Option<Vec<ResolvedRow>>, String>
where
I: IntoIterator<Item = RelationshipVector>,
{
let rows: Vec<RelationshipVector> = rows.into_iter().collect();
if rows.is_empty() {
return Ok(None);
}
require_carried_text_property(graph, relationship_type, text_column)?;
resolve_rows(graph, relationship_type, rows, keys).map(Some)
}
impl From<super::EdgeEmbeddingWriteReport> for RelationshipIngestReport {
fn from(report: super::EdgeEmbeddingWriteReport) -> Self {
RelationshipIngestReport {
stored: report.stored,
dimension: report.dimension,
changed: report.changed,
store_created: report.store_created,
}
}
}
pub fn embed_relationship_texts(
graph: &mut DirGraph,
relationship_type: &str,
text_column: &str,
mode: EmbedMode,
model: &dyn Embedder,
hooks: &EmbedHooks<'_>,
metric: Option<&str>,
) -> Result<EmbedOutcome, EmbedError> {
require_carried_text_property(graph, relationship_type, text_column)
.map_err(EmbedError::Column)?;
let selected = relationship_texts(graph, relationship_type, text_column);
let service = EmbeddingExecutionService {
model,
interrupt: Interrupt::default(),
};
let report = generate_selected(
graph,
EdgeGenerationRequest {
connection_type: relationship_type.to_string(),
text_property: text_column.to_string(),
selected,
mode,
batch_size: hooks.batch_size.max(1),
metric: metric.map(str::to_owned),
},
Some(&service),
Some(hooks),
)?;
Ok(EmbedOutcome {
embedded: report.embedded,
skipped: report.skipped,
skipped_existing: report.skipped_existing,
reembedded_changed: report.reembedded_changed,
dimension: report.dimension,
})
}
fn relationship_texts(
graph: &DirGraph,
relationship_type: &str,
text_column: &str,
) -> Vec<SelectedEdgeText> {
let type_key = InternedKey::from_str(relationship_type);
let _arena_guard = graph.graph.begin_query();
graph
.graph
.edge_indices()
.filter_map(|edge| {
let weight = graph.graph.edge_weight(edge)?;
(weight.connection_type == type_key).then(|| SelectedEdgeText {
edge,
text: match weight.get_property(text_column) {
Some(Value::String(text)) if !text.is_empty() => Some(text.clone()),
_ => None,
},
})
})
.collect()
}
struct DefaultEndpoints {
source: Option<String>,
target: Option<String>,
sources: BTreeSet<String>,
targets: BTreeSet<String>,
}
impl DefaultEndpoints {
fn scan(graph: &DirGraph, relationship_type: &str) -> Self {
let type_key = InternedKey::from_str(relationship_type);
let _arena_guard = graph.graph.begin_query();
let mut seen: HashMap<InternedKey, String> = HashMap::new();
let mut type_of = |node: NodeIndex| -> Option<String> {
let view = graph.graph.node_view(node)?;
Some(
seen.entry(view.node_type())
.or_insert_with(|| view.node_type_str(&graph.interner).to_string())
.clone(),
)
};
let (mut sources, mut targets) = (BTreeSet::new(), BTreeSet::new());
for edge in graph.graph.edge_indices() {
let Some(weight) = graph.graph.edge_weight(edge) else {
continue;
};
if weight.connection_type != type_key {
continue;
}
let Some((source, target)) = graph.graph.edge_endpoints(edge) else {
continue;
};
sources.extend(type_of(source));
targets.extend(type_of(target));
}
let single = |set: &BTreeSet<String>| {
(set.len() == 1)
.then(|| set.iter().next().cloned())
.flatten()
};
DefaultEndpoints {
source: single(&sources),
target: single(&targets),
sources,
targets,
}
}
fn pick<'a>(
&'a self,
explicit: Option<&'a str>,
side: &str,
relationship_type: &str,
position: Option<usize>,
) -> Result<&'a str, String> {
if let Some(explicit) = explicit {
return Ok(explicit);
}
let (single, all) = match side {
"source" => (&self.source, &self.sources),
_ => (&self.target, &self.targets),
};
single.as_deref().ok_or_else(|| {
let found = if all.is_empty() {
format!("no '{relationship_type}' relationship exists")
} else {
format!(
"'{relationship_type}' relationships have {side} nodes of types {}",
all.iter().cloned().collect::<Vec<_>>().join(", ")
)
};
format!(
"{}names no {side} node type, and {found}; address it by (source_type, \
source_id, target_type, target_id)",
RowPrefix(position)
)
})
}
}
fn resolve_rows(
graph: &mut DirGraph,
relationship_type: &str,
rows: Vec<RelationshipVector>,
keys: &RelationshipKeys,
) -> Result<Vec<ResolvedRow>, String> {
let defaults = rows
.iter()
.any(|row| row.source_type.is_none() || row.target_type.is_none())
.then(|| DefaultEndpoints::scan(graph, relationship_type));
let mut endpoint_types: BTreeSet<&str> = BTreeSet::new();
for row in &rows {
endpoint_types.extend(row.source_type.as_deref());
endpoint_types.extend(row.target_type.as_deref());
}
if let Some(defaults) = &defaults {
endpoint_types.extend(defaults.source.as_deref());
endpoint_types.extend(defaults.target.as_deref());
}
let endpoint_types: Vec<String> = endpoint_types.into_iter().map(str::to_owned).collect();
for node_type in &endpoint_types {
graph.build_id_index(node_type);
}
let graph: &DirGraph = graph;
let _arena_guard = graph.graph.begin_query();
let mut resolver = Resolver {
graph,
relationship_type,
key_property: keys.get(relationship_type).map(String::as_str),
parallel: FxHashMap::default(),
};
let mut claimed: FxHashMap<usize, usize> =
FxHashMap::with_capacity_and_hasher(rows.len(), Default::default());
let mut entries = Vec::with_capacity(rows.len());
for (position, row) in rows.into_iter().enumerate() {
let (source_type, target_type) = match &defaults {
Some(defaults) => (
defaults.pick(
row.source_type.as_deref(),
"source",
relationship_type,
Some(position),
)?,
defaults.pick(
row.target_type.as_deref(),
"target",
relationship_type,
Some(position),
)?,
),
None => (
row.source_type.as_deref().unwrap_or_default(),
row.target_type.as_deref().unwrap_or_default(),
),
};
let address = Address {
position: Some(position),
relationship_type,
source_type,
source_id: &row.source_id,
target_type,
target_id: &row.target_id,
};
let edge = resolver.resolve(&address, row.key.as_ref())?;
if let Some(first) = claimed.insert(edge.index(), position) {
return Err(format!(
"rows[{first}] and rows[{position}] both name relationship {}; give each \
relationship one row",
describe_relationship(graph, edge)
));
}
entries.push((edge, row.vector));
}
Ok(entries)
}
struct Address<'a> {
position: Option<usize>,
relationship_type: &'a str,
source_type: &'a str,
source_id: &'a Value,
target_type: &'a str,
target_id: &'a Value,
}
impl std::fmt::Display for Address<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}({} id={})-[:{}]->({} id={})",
RowPrefix(self.position),
self.source_type,
self.source_id,
self.relationship_type,
self.target_type,
self.target_id
)
}
}
struct RowPrefix(Option<usize>);
impl std::fmt::Display for RowPrefix {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.0 {
Some(position) => write!(f, "rows[{position}] "),
None => Ok(()),
}
}
}
pub(crate) fn resolve_address(
graph: &DirGraph,
relationship_type: &str,
address: &RelationshipVector,
keys: &RelationshipKeys,
) -> Result<EdgeIndex, String> {
let defaults = (address.source_type.is_none() || address.target_type.is_none())
.then(|| DefaultEndpoints::scan(graph, relationship_type));
let (source_type, target_type) = match &defaults {
Some(defaults) => (
defaults.pick(
address.source_type.as_deref(),
"source",
relationship_type,
None,
)?,
defaults.pick(
address.target_type.as_deref(),
"target",
relationship_type,
None,
)?,
),
None => (
address.source_type.as_deref().unwrap_or_default(),
address.target_type.as_deref().unwrap_or_default(),
),
};
let _arena_guard = graph.graph.begin_query();
let mut resolver = Resolver {
graph,
relationship_type,
key_property: keys.get(relationship_type).map(String::as_str),
parallel: FxHashMap::default(),
};
resolver.resolve(
&Address {
position: None,
relationship_type,
source_type,
source_id: &address.source_id,
target_type,
target_id: &address.target_id,
},
address.key.as_ref(),
)
}
struct Resolver<'a> {
graph: &'a DirGraph,
relationship_type: &'a str,
key_property: Option<&'a str>,
parallel: FxHashMap<(NodeIndex, NodeIndex), KeyedGroup>,
}
type KeyedGroup = Result<Vec<(EdgeIndex, Value)>, String>;
impl Resolver<'_> {
fn resolve(&mut self, address: &Address<'_>, key: Option<&Value>) -> Result<EdgeIndex, String> {
let graph = self.graph;
let node = |node_type: &str, id: &Value| {
graph
.lookup_by_id_readonly(node_type, id)
.ok_or_else(|| format!("{address}: no '{node_type}' node has id {id}"))
};
let source = node(address.source_type, address.source_id)?;
let target = node(address.target_type, address.target_id)?;
if let Some(keyed) = self.parallel.get(&(source, target)) {
return self.pick_member(keyed, key, source, target, address);
}
let members = group_members(graph, source, target, self.relationship_type);
match members.as_slice() {
[] => Err(format!(
"{address}: no '{}' relationship connects those nodes",
self.relationship_type
)),
[only] => self.check_single(*only, key, address),
_ => {
let keyed = match self.key_property {
Some(property) => group_keys(graph, &members, property),
None => Err(format!(
"relationship_keys names no key property for '{}'",
self.relationship_type
)),
};
let picked = self.pick_member(&keyed, key, source, target, address);
self.parallel.insert((source, target), keyed);
picked
}
}
}
fn check_single(
&self,
edge: EdgeIndex,
key: Option<&Value>,
address: &Address<'_>,
) -> Result<EdgeIndex, String> {
let Some(wanted) = key else {
return Ok(edge);
};
let Some(property) = self.key_property else {
return Err(format!(
"{address} gives key {wanted}, but relationship_keys names no key property \
for '{}'",
self.relationship_type
));
};
match key_value(self.graph, edge, property) {
Some(found) if found == *wanted => Ok(edge),
found => Err(format!(
"{address}: the '{}' relationship between those nodes has {property}={}, \
not {wanted}",
self.relationship_type,
found.map_or_else(|| "no value".to_string(), |value| value.to_string())
)),
}
}
fn pick_member(
&self,
keyed: &KeyedGroup,
key: Option<&Value>,
source: NodeIndex,
target: NodeIndex,
address: &Address<'_>,
) -> Result<EdgeIndex, String> {
let refuse = |reason: &str| {
let count = group_members(self.graph, source, target, self.relationship_type).len();
let group = describe_group(self.graph, self.relationship_type, source, target, count);
format!(
"{address} is ambiguous: {group}, and {reason}. A parallel group is written \
only through a key property whose value is unique within the group: pass \
relationship_keys={{'{}': '<property>'}} and give each row its key",
self.relationship_type
)
};
let keyed = keyed.as_ref().map_err(|reason| refuse(reason))?;
let Some(wanted) = key else {
return Err(refuse("the row gives no key value"));
};
let property = self.key_property.unwrap_or_default();
keyed
.iter()
.find(|(_, value)| value == wanted)
.map(|(edge, _)| *edge)
.ok_or_else(|| {
let count = keyed.len();
let group =
describe_group(self.graph, self.relationship_type, source, target, count);
format!("{address}: {group}, and none has {property}={wanted}")
})
}
}
#[cfg(test)]
#[path = "edge_embedding_ingest_tests.rs"]
mod tests;