chore: checkpoint ai search comments and i18n foundation
This commit is contained in:
@@ -13,6 +13,10 @@ mod m20260328_000002_create_site_settings;
|
||||
mod m20260328_000003_add_site_url_to_site_settings;
|
||||
mod m20260328_000004_add_posts_search_index;
|
||||
mod m20260328_000005_categories;
|
||||
mod m20260328_000006_add_ai_to_site_settings;
|
||||
mod m20260328_000007_create_ai_chunks;
|
||||
mod m20260328_000008_enable_pgvector_for_ai_chunks;
|
||||
mod m20260328_000009_add_paragraph_comments;
|
||||
pub struct Migrator;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@@ -30,6 +34,10 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20260328_000003_add_site_url_to_site_settings::Migration),
|
||||
Box::new(m20260328_000004_add_posts_search_index::Migration),
|
||||
Box::new(m20260328_000005_categories::Migration),
|
||||
Box::new(m20260328_000006_add_ai_to_site_settings::Migration),
|
||||
Box::new(m20260328_000007_create_ai_chunks::Migration),
|
||||
Box::new(m20260328_000008_enable_pgvector_for_ai_chunks::Migration),
|
||||
Box::new(m20260328_000009_add_paragraph_comments::Migration),
|
||||
// inject-above (do not remove this comment)
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let table = Alias::new("site_settings");
|
||||
|
||||
if !manager.has_column("site_settings", "ai_enabled").await? {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(table.clone())
|
||||
.add_column(
|
||||
ColumnDef::new(Alias::new("ai_enabled"))
|
||||
.boolean()
|
||||
.null()
|
||||
.default(false),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !manager.has_column("site_settings", "ai_provider").await? {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(table.clone())
|
||||
.add_column(ColumnDef::new(Alias::new("ai_provider")).string().null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !manager.has_column("site_settings", "ai_api_base").await? {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(table.clone())
|
||||
.add_column(ColumnDef::new(Alias::new("ai_api_base")).string().null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !manager.has_column("site_settings", "ai_api_key").await? {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(table.clone())
|
||||
.add_column(ColumnDef::new(Alias::new("ai_api_key")).text().null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !manager.has_column("site_settings", "ai_chat_model").await? {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(table.clone())
|
||||
.add_column(ColumnDef::new(Alias::new("ai_chat_model")).string().null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !manager
|
||||
.has_column("site_settings", "ai_embedding_model")
|
||||
.await?
|
||||
{
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(table.clone())
|
||||
.add_column(
|
||||
ColumnDef::new(Alias::new("ai_embedding_model"))
|
||||
.string()
|
||||
.null(),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !manager
|
||||
.has_column("site_settings", "ai_system_prompt")
|
||||
.await?
|
||||
{
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(table.clone())
|
||||
.add_column(ColumnDef::new(Alias::new("ai_system_prompt")).text().null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !manager.has_column("site_settings", "ai_top_k").await? {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(table.clone())
|
||||
.add_column(ColumnDef::new(Alias::new("ai_top_k")).integer().null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !manager.has_column("site_settings", "ai_chunk_size").await? {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(table.clone())
|
||||
.add_column(ColumnDef::new(Alias::new("ai_chunk_size")).integer().null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !manager
|
||||
.has_column("site_settings", "ai_last_indexed_at")
|
||||
.await?
|
||||
{
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(table)
|
||||
.add_column(
|
||||
ColumnDef::new(Alias::new("ai_last_indexed_at"))
|
||||
.timestamp_with_time_zone()
|
||||
.null(),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let table = Alias::new("site_settings");
|
||||
|
||||
for column in [
|
||||
"ai_last_indexed_at",
|
||||
"ai_chunk_size",
|
||||
"ai_top_k",
|
||||
"ai_system_prompt",
|
||||
"ai_embedding_model",
|
||||
"ai_chat_model",
|
||||
"ai_api_key",
|
||||
"ai_api_base",
|
||||
"ai_provider",
|
||||
"ai_enabled",
|
||||
] {
|
||||
if manager.has_column("site_settings", column).await? {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(table.clone())
|
||||
.drop_column(Alias::new(column))
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
33
backend/migration/src/m20260328_000007_create_ai_chunks.rs
Normal file
33
backend/migration/src/m20260328_000007_create_ai_chunks.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use loco_rs::schema::*;
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
create_table(
|
||||
manager,
|
||||
"ai_chunks",
|
||||
&[
|
||||
("id", ColType::PkAuto),
|
||||
("source_slug", ColType::String),
|
||||
("source_title", ColType::StringNull),
|
||||
("source_path", ColType::StringNull),
|
||||
("source_type", ColType::String),
|
||||
("chunk_index", ColType::Integer),
|
||||
("content", ColType::Text),
|
||||
("content_preview", ColType::StringNull),
|
||||
("embedding", ColType::JsonBinaryNull),
|
||||
("word_count", ColType::IntegerNull),
|
||||
],
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
drop_table(manager, "ai_chunks").await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
const AI_CHUNKS_TABLE: &str = "ai_chunks";
|
||||
const VECTOR_INDEX_NAME: &str = "idx_ai_chunks_embedding_hnsw";
|
||||
const EMBEDDING_DIMENSION: i32 = 384;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.get_connection()
|
||||
.execute_unprepared("CREATE EXTENSION IF NOT EXISTS vector")
|
||||
.await?;
|
||||
|
||||
if manager.has_column(AI_CHUNKS_TABLE, "embedding").await? {
|
||||
manager
|
||||
.get_connection()
|
||||
.execute_unprepared("ALTER TABLE ai_chunks DROP COLUMN embedding")
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !manager.has_column(AI_CHUNKS_TABLE, "embedding").await? {
|
||||
manager
|
||||
.get_connection()
|
||||
.execute_unprepared(&format!(
|
||||
"ALTER TABLE ai_chunks ADD COLUMN embedding vector({EMBEDDING_DIMENSION})"
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
|
||||
manager
|
||||
.get_connection()
|
||||
.execute_unprepared("TRUNCATE TABLE ai_chunks RESTART IDENTITY")
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.get_connection()
|
||||
.execute_unprepared(&format!(
|
||||
"CREATE INDEX IF NOT EXISTS {VECTOR_INDEX_NAME} ON ai_chunks USING hnsw (embedding vector_cosine_ops)"
|
||||
))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.get_connection()
|
||||
.execute_unprepared(&format!("DROP INDEX IF EXISTS {VECTOR_INDEX_NAME}"))
|
||||
.await?;
|
||||
|
||||
if manager.has_column(AI_CHUNKS_TABLE, "embedding").await? {
|
||||
manager
|
||||
.get_connection()
|
||||
.execute_unprepared("ALTER TABLE ai_chunks DROP COLUMN embedding")
|
||||
.await?;
|
||||
}
|
||||
|
||||
manager
|
||||
.get_connection()
|
||||
.execute_unprepared("ALTER TABLE ai_chunks ADD COLUMN embedding jsonb")
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
109
backend/migration/src/m20260328_000009_add_paragraph_comments.rs
Normal file
109
backend/migration/src/m20260328_000009_add_paragraph_comments.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
const TABLE: &str = "comments";
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let table = Alias::new(TABLE);
|
||||
|
||||
if !manager.has_column(TABLE, "scope").await? {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(table.clone())
|
||||
.add_column(
|
||||
ColumnDef::new(Alias::new("scope"))
|
||||
.string()
|
||||
.not_null()
|
||||
.default("article"),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !manager.has_column(TABLE, "paragraph_key").await? {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(table.clone())
|
||||
.add_column(ColumnDef::new(Alias::new("paragraph_key")).string().null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !manager.has_column(TABLE, "paragraph_excerpt").await? {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(table.clone())
|
||||
.add_column(ColumnDef::new(Alias::new("paragraph_excerpt")).string().null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !manager.has_column(TABLE, "reply_to_comment_id").await? {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(table.clone())
|
||||
.add_column(
|
||||
ColumnDef::new(Alias::new("reply_to_comment_id"))
|
||||
.integer()
|
||||
.null(),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
manager
|
||||
.get_connection()
|
||||
.execute_unprepared(
|
||||
"UPDATE comments SET scope = 'article' WHERE scope IS NULL OR trim(scope) = ''",
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.get_connection()
|
||||
.execute_unprepared(
|
||||
"CREATE INDEX IF NOT EXISTS idx_comments_post_scope_paragraph ON comments (post_slug, scope, paragraph_key)",
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.get_connection()
|
||||
.execute_unprepared("DROP INDEX IF EXISTS idx_comments_post_scope_paragraph")
|
||||
.await?;
|
||||
|
||||
for column in [
|
||||
"reply_to_comment_id",
|
||||
"paragraph_excerpt",
|
||||
"paragraph_key",
|
||||
"scope",
|
||||
] {
|
||||
if manager.has_column(TABLE, column).await? {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(Alias::new(TABLE))
|
||||
.drop_column(Alias::new(column))
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user