Current section
Files
Jump to
Current section
Files
lib/mix/tasks/portfolio.install.ex
defmodule Mix.Tasks.Portfolio.Install do
@moduledoc """
Installs PortfolioIndex into a Phoenix application.
## Usage
mix portfolio.install
## What it does
1. Creates required database migrations
2. Prints configuration instructions
3. Provides next steps for setup
## Options
- `--repo` - Ecto repo module (default: inferred from app)
- `--dimension` - Embedding vector dimension (default: 384)
- `--no-migrations` - Skip migration generation
"""
use Mix.Task
import Mix.Generator
@shortdoc "Install PortfolioIndex in your application"
@migration_template """
defmodule <%= @repo %>.Migrations.CreatePortfolioTables do
use Ecto.Migration
def up do
execute "CREATE EXTENSION IF NOT EXISTS vector"
create table(:portfolio_collections, primary_key: false) do
add :id, :binary_id, primary_key: true
add :name, :string, null: false
add :description, :text
add :metadata, :map, default: %{}
timestamps(type: :utc_datetime)
end
create unique_index(:portfolio_collections, [:name])
create table(:portfolio_documents, primary_key: false) do
add :id, :binary_id, primary_key: true
add :source_id, :string
add :content_hash, :string
add :title, :string
add :source_path, :string
add :metadata, :map, default: %{}
add :status, :string, default: "pending"
add :error_message, :text
add :chunk_count, :integer, default: 0
add :collection_id, references(:portfolio_collections, type: :binary_id, on_delete: :nilify_all)
timestamps(type: :utc_datetime)
end
create index(:portfolio_documents, [:source_id])
create index(:portfolio_documents, [:content_hash])
create index(:portfolio_documents, [:status])
create index(:portfolio_documents, [:collection_id])
create table(:portfolio_chunks, primary_key: false) do
add :id, :binary_id, primary_key: true
add :content, :text, null: false
add :embedding, :vector, size: <%= @dimension %>, null: true
add :chunk_index, :integer, default: 0
add :token_count, :integer
add :start_char, :integer
add :end_char, :integer
add :metadata, :map, default: %{}
add :document_id, references(:portfolio_documents, type: :binary_id, on_delete: :delete_all)
timestamps(type: :utc_datetime)
end
create index(:portfolio_chunks, [:document_id])
execute \"\"\"
CREATE INDEX portfolio_chunks_embedding_idx ON portfolio_chunks
USING hnsw (embedding vector_cosine_ops)
\"\"\"
end
def down do
drop table(:portfolio_chunks)
drop table(:portfolio_documents)
drop table(:portfolio_collections)
# Note: We don't drop the vector extension as it may be used by other tables
end
end
"""
@impl Mix.Task
def run(args) do
{opts, _, _} =
OptionParser.parse(args,
strict: [
repo: :string,
dimension: :integer,
no_migrations: :boolean
]
)
repo = opts[:repo] || infer_repo()
dimension = opts[:dimension] || 384
skip_migrations = opts[:no_migrations] || false
unless skip_migrations do
generate_migration(repo, dimension)
end
print_instructions(repo, dimension)
end
defp generate_migration(repo, dimension) do
repo_underscore =
repo
|> String.replace(".", "/")
|> Macro.underscore()
|> String.replace("/", "_")
migrations_path = Path.join(["priv", repo_underscore, "migrations"])
File.mkdir_p!(migrations_path)
timestamp = Calendar.strftime(DateTime.utc_now(), "%Y%m%d%H%M%S")
filename = "#{timestamp}_create_portfolio_tables.exs"
path = Path.join(migrations_path, filename)
content = EEx.eval_string(@migration_template, assigns: [repo: repo, dimension: dimension])
create_file(path, content)
end
defp print_instructions(repo, dimension) do
Mix.shell().info("""
PortfolioIndex installation complete!
Configuration for #{repo} with #{dimension} dimensions.
== Next Steps ==
1. Run the migration:
mix ecto.migrate
2. Configure pgvector types in your repo config:
# config/config.exs
config :my_app, #{repo},
types: MyApp.PostgrexTypes
3. Create the types module (if not already present):
# lib/my_app/postgrex_types.ex
Postgrex.Types.define(
MyApp.PostgrexTypes,
[Pgvector.Extensions.Vector] ++ Ecto.Adapters.Postgres.extensions(),
[]
)
4. Configure the embedder in your config:
# config/config.exs
config :portfolio_index,
embedder: PortfolioIndex.Adapters.Embedder.Gemini,
embedding: [
default_dimensions: #{dimension}
]
5. Start using PortfolioIndex:
# Ingest a document
PortfolioIndex.Maintenance.diagnostics(#{repo})
== Optional: Broadway Pipelines ==
For async document processing, add the pipelines to your supervision tree:
children = [
#{repo},
{PortfolioIndex.Pipelines.Ingestion, [repo: #{repo}]},
{PortfolioIndex.Pipelines.Embedding, [repo: #{repo}]}
]
""")
end
defp infer_repo do
case Mix.Project.config()[:app] do
nil ->
"MyApp.Repo"
app ->
app
|> to_string()
|> Macro.camelize()
|> Kernel.<>(".Repo")
end
end
end