2024-12-13 08:15:21 -05:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
# We don't have AR objects for our embeddings, so this class
|
|
|
|
# acts as an intermediary between us and the DB.
|
|
|
|
# It lets us retrieve embeddings either symmetrically and asymmetrically,
|
|
|
|
# and also store them.
|
|
|
|
|
|
|
|
module DiscourseAi
|
|
|
|
module Embeddings
|
|
|
|
class Schema
|
|
|
|
TOPICS_TABLE = "ai_topic_embeddings"
|
|
|
|
POSTS_TABLE = "ai_post_embeddings"
|
|
|
|
RAG_DOCS_TABLE = "ai_document_fragment_embeddings"
|
|
|
|
|
|
|
|
def self.for(
|
|
|
|
target_klass,
|
2024-12-16 07:55:39 -05:00
|
|
|
vector_def: DiscourseAi::Embeddings::VectorRepresentations::Base.current_representation
|
2024-12-13 08:15:21 -05:00
|
|
|
)
|
|
|
|
case target_klass&.name
|
|
|
|
when "Topic"
|
2024-12-16 07:55:39 -05:00
|
|
|
new(TOPICS_TABLE, "topic_id", vector_def)
|
2024-12-13 08:15:21 -05:00
|
|
|
when "Post"
|
2024-12-16 07:55:39 -05:00
|
|
|
new(POSTS_TABLE, "post_id", vector_def)
|
2024-12-13 08:15:21 -05:00
|
|
|
when "RagDocumentFragment"
|
2024-12-16 07:55:39 -05:00
|
|
|
new(RAG_DOCS_TABLE, "rag_document_fragment_id", vector_def)
|
2024-12-13 08:15:21 -05:00
|
|
|
else
|
|
|
|
raise ArgumentError, "Invalid target type for embeddings"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-12-16 07:55:39 -05:00
|
|
|
def initialize(table, target_column, vector_def)
|
2024-12-13 08:15:21 -05:00
|
|
|
@table = table
|
|
|
|
@target_column = target_column
|
2024-12-16 07:55:39 -05:00
|
|
|
@vector_def = vector_def
|
2024-12-13 08:15:21 -05:00
|
|
|
end
|
|
|
|
|
2024-12-16 07:55:39 -05:00
|
|
|
attr_reader :table, :target_column, :vector_def
|
2024-12-13 08:15:21 -05:00
|
|
|
|
|
|
|
def find_by_embedding(embedding)
|
2024-12-16 07:55:39 -05:00
|
|
|
DB.query(
|
|
|
|
<<~SQL,
|
2024-12-13 08:15:21 -05:00
|
|
|
SELECT *
|
|
|
|
FROM #{table}
|
|
|
|
WHERE
|
|
|
|
model_id = :vid AND strategy_id = :vsid
|
|
|
|
ORDER BY
|
|
|
|
embeddings::halfvec(#{dimensions}) #{pg_function} '[:query_embedding]'::halfvec(#{dimensions})
|
|
|
|
LIMIT 1
|
|
|
|
SQL
|
2024-12-16 07:55:39 -05:00
|
|
|
query_embedding: embedding,
|
|
|
|
vid: vector_def.id,
|
|
|
|
vsid: vector_def.strategy_id,
|
|
|
|
).first
|
2024-12-13 08:15:21 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def find_by_target(target)
|
2024-12-16 07:55:39 -05:00
|
|
|
DB.query(
|
|
|
|
<<~SQL,
|
2024-12-13 08:15:21 -05:00
|
|
|
SELECT *
|
|
|
|
FROM #{table}
|
|
|
|
WHERE
|
|
|
|
model_id = :vid AND
|
|
|
|
strategy_id = :vsid AND
|
|
|
|
#{target_column} = :target_id
|
|
|
|
LIMIT 1
|
|
|
|
SQL
|
2024-12-16 07:55:39 -05:00
|
|
|
target_id: target.id,
|
|
|
|
vid: vector_def.id,
|
|
|
|
vsid: vector_def.strategy_id,
|
|
|
|
).first
|
2024-12-13 08:15:21 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def asymmetric_similarity_search(embedding, limit:, offset:)
|
|
|
|
builder = DB.build(<<~SQL)
|
|
|
|
WITH candidates AS (
|
|
|
|
SELECT
|
|
|
|
#{target_column},
|
|
|
|
embeddings::halfvec(#{dimensions}) AS embeddings
|
|
|
|
FROM
|
|
|
|
#{table}
|
|
|
|
/*join*/
|
|
|
|
/*where*/
|
|
|
|
ORDER BY
|
|
|
|
binary_quantize(embeddings)::bit(#{dimensions}) <~> binary_quantize('[:query_embedding]'::halfvec(#{dimensions}))
|
|
|
|
LIMIT :candidates_limit
|
|
|
|
)
|
|
|
|
SELECT
|
|
|
|
#{target_column},
|
|
|
|
embeddings::halfvec(#{dimensions}) #{pg_function} '[:query_embedding]'::halfvec(#{dimensions}) AS distance
|
|
|
|
FROM
|
|
|
|
candidates
|
|
|
|
ORDER BY
|
|
|
|
embeddings::halfvec(#{dimensions}) #{pg_function} '[:query_embedding]'::halfvec(#{dimensions})
|
|
|
|
LIMIT :limit
|
|
|
|
OFFSET :offset
|
|
|
|
SQL
|
|
|
|
|
|
|
|
builder.where(
|
|
|
|
"model_id = :model_id AND strategy_id = :strategy_id",
|
2024-12-16 07:55:39 -05:00
|
|
|
model_id: vector_def.id,
|
|
|
|
strategy_id: vector_def.strategy_id,
|
2024-12-13 08:15:21 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
yield(builder) if block_given?
|
|
|
|
|
|
|
|
if table == RAG_DOCS_TABLE
|
|
|
|
# A too low limit exacerbates the the recall loss of binary quantization
|
|
|
|
candidates_limit = [limit * 2, 100].max
|
|
|
|
else
|
|
|
|
candidates_limit = limit * 2
|
|
|
|
end
|
|
|
|
|
|
|
|
builder.query(
|
|
|
|
query_embedding: embedding,
|
|
|
|
candidates_limit: candidates_limit,
|
|
|
|
limit: limit,
|
|
|
|
offset: offset,
|
|
|
|
)
|
|
|
|
rescue PG::Error => e
|
|
|
|
Rails.logger.error("Error #{e} querying embeddings for model #{name}")
|
|
|
|
raise MissingEmbeddingError
|
|
|
|
end
|
|
|
|
|
|
|
|
def symmetric_similarity_search(record)
|
|
|
|
builder = DB.build(<<~SQL)
|
|
|
|
WITH le_target AS (
|
|
|
|
SELECT
|
|
|
|
embeddings
|
|
|
|
FROM
|
|
|
|
#{table}
|
|
|
|
WHERE
|
|
|
|
model_id = :vid AND
|
|
|
|
strategy_id = :vsid AND
|
|
|
|
#{target_column} = :target_id
|
|
|
|
LIMIT 1
|
|
|
|
)
|
|
|
|
SELECT #{target_column} FROM (
|
|
|
|
SELECT
|
|
|
|
#{target_column}, embeddings
|
|
|
|
FROM
|
|
|
|
#{table}
|
|
|
|
/*join*/
|
|
|
|
/*where*/
|
|
|
|
ORDER BY
|
|
|
|
binary_quantize(embeddings)::bit(#{dimensions}) <~> (
|
|
|
|
SELECT
|
|
|
|
binary_quantize(embeddings)::bit(#{dimensions})
|
|
|
|
FROM
|
|
|
|
le_target
|
|
|
|
LIMIT 1
|
|
|
|
)
|
|
|
|
LIMIT 200
|
|
|
|
) AS widenet
|
|
|
|
ORDER BY
|
|
|
|
embeddings::halfvec(#{dimensions}) #{pg_function} (
|
|
|
|
SELECT
|
|
|
|
embeddings::halfvec(#{dimensions})
|
|
|
|
FROM
|
|
|
|
le_target
|
|
|
|
LIMIT 1
|
|
|
|
)
|
|
|
|
LIMIT 100;
|
|
|
|
SQL
|
|
|
|
|
|
|
|
builder.where("model_id = :vid AND strategy_id = :vsid")
|
|
|
|
|
|
|
|
yield(builder) if block_given?
|
|
|
|
|
2024-12-16 07:55:39 -05:00
|
|
|
builder.query(vid: vector_def.id, vsid: vector_def.strategy_id, target_id: record.id)
|
2024-12-13 08:15:21 -05:00
|
|
|
rescue PG::Error => e
|
|
|
|
Rails.logger.error("Error #{e} querying embeddings for model #{name}")
|
|
|
|
raise MissingEmbeddingError
|
|
|
|
end
|
|
|
|
|
|
|
|
def store(record, embedding, digest)
|
|
|
|
DB.exec(
|
|
|
|
<<~SQL,
|
|
|
|
INSERT INTO #{table} (#{target_column}, model_id, model_version, strategy_id, strategy_version, digest, embeddings, created_at, updated_at)
|
|
|
|
VALUES (:target_id, :model_id, :model_version, :strategy_id, :strategy_version, :digest, '[:embeddings]', :now, :now)
|
|
|
|
ON CONFLICT (model_id, strategy_id, #{target_column})
|
|
|
|
DO UPDATE SET
|
|
|
|
model_version = :model_version,
|
|
|
|
strategy_version = :strategy_version,
|
|
|
|
digest = :digest,
|
|
|
|
embeddings = '[:embeddings]',
|
|
|
|
updated_at = :now
|
|
|
|
SQL
|
|
|
|
target_id: record.id,
|
2024-12-16 07:55:39 -05:00
|
|
|
model_id: vector_def.id,
|
|
|
|
model_version: vector_def.version,
|
|
|
|
strategy_id: vector_def.strategy_id,
|
|
|
|
strategy_version: vector_def.strategy_version,
|
2024-12-13 08:15:21 -05:00
|
|
|
digest: digest,
|
|
|
|
embeddings: embedding,
|
|
|
|
now: Time.zone.now,
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
2024-12-16 07:55:39 -05:00
|
|
|
delegate :dimensions, :pg_function, to: :vector_def
|
2024-12-13 08:15:21 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|