A .NET search engine that matches construction BOM (Bill of Materials) line items to product recommendations from a Supabase PostgreSQL database. Runs as a console CLI for development and as AWS Lambda functions in production, connected via Amazon MQ RabbitMQ.
ββββββββββββββββ RabbitMQ ββββββββββββββββββββ RabbitMQ βββββββββββββββββββ
β Frontend β ββextract.requestββΆ β BOM Extraction β ββextract.resultβββΆ β Search Lambda β
β (submit β β Lambda β β (product search β
β BOM file) β β (Bedrock Nova) β β per BOM item) β
ββββββββββββββββ ββββββββββββββββββββ βββββββββ¬ββββββββββ
β
ββββββββββββββ΄βββββββββββββ
β β
search.result search.zero-result
β β
βΌ βΌ
βββββββββββββββββββ βββββββββββββββββββββ
β results-queue β β zero-results-queueβ
β (β₯1 products) β β (0 products) β
βββββββββββββββββββ βββββββββββββββββββββ
| Exchange | Queue | Routing Key | Purpose |
|---|---|---|---|
bom.extraction |
bom-extraction-queue |
extract.request |
Incoming BOM file extraction requests |
bom.extraction |
bom-extraction-result-queue |
extract.result |
Extracted BOM line items β triggers Search Lambda |
sourcing.engine |
sourcing-engine-search-results-queue |
search.result |
Products found (β₯1 match per item) |
sourcing.engine |
sourcing-engine-search-zero-results-queue |
search.zero-result |
Items with 0 matches |
bom.extraction.dlx |
bom-extraction-poison-queue |
extract.poison |
Extraction failures |
sourcing.engine.dlx |
sourcing-engine-poison-queue |
search.poison |
Search failures (DLX on result queue) |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Console / Lambda Entry Points β
β β’ CLI for dev/testing β
β β’ BOM Extraction Lambda (Bedrock Nova Pro) β
β β’ Search Lambda (SearchOrchestrator) β
ββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββ
β Core Layer β
β β’ Domain models (BomItem, Product, SearchResult) β
β β’ Business logic services β
β - SizeCalculator (bidirectional imperial β metric) β
β - SynonymExpander (material terminology) β
β - SearchOrchestrator (8-step search pipeline) β
β β’ Repository interfaces (contracts) β
ββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββ
β Data Layer β
β β’ Npgsql repository implementations β
β β’ Dynamic schema discovery β
β β’ Parallel vendor query execution β
β β’ Supabase PostgreSQL connection factory β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- Dependency Inversion: Core layer defines interfaces; Data layer implements them
- Single Responsibility: Each service has one clear purpose
- Testability: Interface-based design enables easy mocking and unit testing
- Scalability: Parallel queries across vendor schemas for performance
The application implements a sophisticated search pipeline based on the documented search logic:
graph TD
A[BOM Input: "8 inch masonry block"] --> B[Step 1: Parse & Normalize]
B --> C[Step 2: Find Material Family]
C --> D[Step 3: Resolve CSI Code]
D --> E[Step 4: Find Vendors]
E --> F[Step 5: Filter by Size]
F --> G[Step 6: Get Product Intelligence]
G --> H[Step 7: Get Deep Vendor Data]
H --> I[Step 8: Aggregate Results]
I --> J[JSON Output]
-
Parse & Normalize Input
- Extract keywords:
["masonry", "block"] - Calculate size variants:
["8\"", "8 inch", "20cm", "200mm"] - Expand synonyms:
["cmu", "concrete block", "masonry unit"]
- Extract keywords:
-
Find Material Family
- Query
cm_master_materialstable using keywords - Result:
family_label = "cmu_blocks"
- Query
-
Resolve CSI Code
- Match family to CSI MasterFormat
- Result:
csi_code = "042200"(Concrete Unit Masonry)
-
Find Vendors
- Query
productstable filtered by family - Join with
vendorstable for metadata
- Query
-
Filter by Size
- Apply size patterns to
model_namecolumn - Uses ILIKE pattern matching:
%20cm%,%8"%
- Apply size patterns to
-
Get Product Intelligence
- Query
product_knowledgetable - Retrieve use cases, specifications, applications
- Query
-
Get Deep Vendor Data (Parallel)
- Query all 12 vendor schemas simultaneously using
Task.WhenAll - Each schema:
{vendor}.products_enriched - Extract:
use_when,key_features,technical_specs,performance_data
- Query all 12 vendor schemas simultaneously using
-
Aggregate Results
- Combine product base data with enriched intelligence
- Return unified JSON response
SourcingEngine/
βββ SourcingEngine.sln
β
βββ src/
β βββ SourcingEngine.Common/ # Shared models & contracts
β β βββ Models/
β β βββ QueueMessages.cs # DTOs for RabbitMQ messages
β β
β βββ SourcingEngine.Core/ # Domain & Business Logic
β β βββ Models/
β β β βββ BomItem.cs # Normalized input
β β β βββ Product.cs # Base product
β β β βββ ProductEnriched.cs # Vendor intelligence
β β β βββ ProductMatch.cs # Search result item
β β β βββ SearchResult.cs # Complete search response
β β β βββ MaterialFamily.cs # Material taxonomy
β β βββ Repositories/ # Interfaces
β β βββ Services/
β β βββ SizeCalculator.cs # Bidirectional size conversion
β β βββ SynonymExpander.cs # Terminology expansion
β β βββ InputNormalizer.cs # BOM text processing
β β βββ SearchOrchestrator.cs # 8-step search pipeline
β β
β βββ SourcingEngine.Data/ # Database Access Layer
β β βββ NpgsqlConnectionFactory.cs # Connection management
β β βββ Repositories/ # Npgsql implementations
β β
β βββ SourcingEngine.Console/ # CLI Entry Point
β β βββ Program.cs
β β βββ appsettings.json
β β
β βββ SourcingEngine.BomExtraction/ # BOM extraction service
β βββ SourcingEngine.BomExtraction.Lambda/ # BOM Extraction Lambda
β β βββ Function.cs # Lambda handler
β β βββ Dockerfile # Container image
β β βββ local/
β β βββ rabbitmq-definitions.json # Local RabbitMQ topology
β β βββ test-event-template.json # Sample event for replay
β βββ SourcingEngine.BomExtraction.Lambda.LocalRunner/ # F5 debug harness
β β
β βββ SourcingEngine.Search.Lambda/ # β
Search Lambda
β β βββ Function.cs # Lambda handler
β β βββ Dockerfile # Container image
β β βββ Configuration/
β β β βββ SearchLambdaSettings.cs # Broker/exchange config
β β βββ Services/
β β βββ RabbitMqSearchResultPublisher.cs # Publishes to results queues
β βββ SourcingEngine.Search.Lambda.LocalRunner/ # F5 debug harness
β βββ LocalRunner.cs # Live consumer + event replay
β
βββ tests/
β βββ SourcingEngine.Tests/ # Core unit/integration tests
β βββ SourcingEngine.BomExtraction.Tests/
β βββ SourcingEngine.BomExtraction.Lambda.Tests/
β βββ SourcingEngine.Search.Lambda.Tests/ # β
Search Lambda tests (16)
β βββ FunctionTests.cs # Handler logic (10 tests)
β βββ QueueMessageSerializationTests.cs # JSON contracts (5 tests)
β
βββ infra/
β βββ BomExtractionLambdaCdk/ # CDK infrastructure (C#)
β βββ BomExtractionLambdaStack.cs # Both Lambdas + topology
β βββ cdk.json # Context values
β βββ lambda/
β βββ rabbitmq-topology/
β βββ index.py # Custom Resource handler
β
βββ scripts/
βββ deploy-lambda.sh # BOM Extraction deploy
βββ deploy-sourcing-lambda.sh # Search Lambda deploy
Automatically converts between imperial and metric units in both directions:
Input: "8 inch masonry block"
β Output: ["8\"", "8 inch", "20cm", "20 cm", "200mm", "200 mm"]
Input: "20cm concrete block"
β Output: ["20cm", "20 cm", "200mm", "200 mm", "8\"", "8 inch"]
Supported conversions:
- 4" β 10cm β 100mm
- 6" β 15cm β 150mm
- 8" β 20cm β 200mm
- 10" β 25cm β 250mm
- 12" β 30cm β 300mm
Expands construction terminology for comprehensive search coverage:
"masonry block" β ["cmu", "concrete block", "masonry unit", "block"]
"floor truss" β ["joist", "i-joist", "bci", "floor joist"]
"stucco" β ["eifs", "plaster", "stucco system"]
"railing" β ["guardrail", "handrail", "balustrade"]
Automatically discovers all vendor schemas at startup:
- Queries:
information_schema.tables WHERE table_name = 'products_enriched' - Discovers: 12 vendor schemas (boehmers, sto, kawneer, boise_cascade, etc.)
- Caches results for the session
Queries all vendor schemas simultaneously for optimal performance:
var tasks = schemas.Select(schema => QuerySchemaAsync(schema, productIds));
var results = await Task.WhenAll(tasks);Resilient error handling: If one schema query fails, continues with partial results from other schemas.
Uses Microsoft.Extensions.Logging throughout:
[INFO] Starting search for: 8" Masonry block
[DEBUG] Step 1: Normalizing input...
[INFO] Extracted 4 keywords, 6 size variants, 12 synonyms
[DEBUG] Step 2: Finding material family...
[INFO] Found material family: cmu_blocks (Concrete Masonry Units)
[INFO] Querying 12 vendor schemas in parallel...
[INFO] Found 7 products matching criteria
[INFO] Search completed in 245ms with 7 matches
- .NET 9.0 - Target framework
Microsoft.Extensions.Logging.Abstractions8.0.0
Npgsql8.0.6 - PostgreSQL driver for .NETMicrosoft.Extensions.Options8.0.0 - Configuration binding
Microsoft.Extensions.Hosting8.0.0 - DI & configurationMicrosoft.Extensions.Configuration.Json8.0.0 - JSON config
xunit2.7.0 - Test frameworkMicrosoft.NET.Test.Sdk17.9.0 - Test runner
- .NET 9.0 SDK installed
- Access to Supabase PostgreSQL database
- Network access to Supabase (port 5432 or session pooler)
Update appsettings.json with your Supabase connection string:
{
"Database": {
"ConnectionString": "Host=aws-1-us-east-1.pooler.supabase.com;Port=5432;Database=postgres;Username=postgres.dtxsieykjcvspzbsrrln;Password=YOUR_PASSWORD;SSL Mode=Require;Trust Server Certificate=true"
}
}Connection Options:
- Direct Connection (port 5432): Best for VMs/servers with static IPs
- Session Pooler (port 5432): Recommended for IPv4 networks with connection pooling
- Transaction Pooler (port 6543): For serverless environments
Get your connection string from Supabase Dashboard β Settings β Database.
cd SourcingEngine
dotnet restore
dotnet build# Basic search
dotnet run --project src/SourcingEngine.Console -- "8 inch masonry block"
# With metric size
dotnet run --project src/SourcingEngine.Console -- "20cm concrete block"
# Complex BOM item
dotnet run --project src/SourcingEngine.Console -- "Pre Engineered Wood Floor Trusses"
# Stucco system
dotnet run --project src/SourcingEngine.Console -- "5/8 stucco on block"
# Railing
dotnet run --project src/SourcingEngine.Console -- "Ext Railing"{
"query": "8 inch masonry block",
"sizeVariants": ["8\"", "8 inch", "20cm", "20 cm", "200mm", "200 mm"],
"keywords": ["masonry", "block", "cmu", "concrete block", "masonry unit"],
"familyLabel": "cmu_blocks",
"csiCode": "042200",
"matchCount": 7,
"matches": [
{
"productId": "uuid-here",
"vendor": "Boehmers Block",
"modelName": "Stretcher 20cm (BOE-STD-6)",
"modelCode": "BOE-STD-6",
"csiCode": "042200",
"useWhen": "Standard load-bearing walls in residential construction",
"keyFeatures": ["High compressive strength", "Thermal mass benefits"],
"technicalSpecs": {
"width": "20cm",
"height": "20cm",
"length": "40cm"
},
"sourceSchema": "boehmers"
}
],
"executionTimeMs": 245,
"warnings": []
}dotnet testdotnet test --filter "Category!=Integration"27 unit tests covering:
- Size calculator (imperial β metric conversion)
- Synonym expander (terminology expansion)
- Input normalizer (keyword extraction)
dotnet test --filter "Category=Integration"Integration tests validate:
- Schema discovery (finds 12+ vendor schemas)
- Material family resolution
- Product repository queries
- Parallel vendor data fetching
dotnet test --filter "Category=Acceptance"5 acceptance tests based on documented test cases:
- β Masonry block search: β₯3 matches from β₯2 vendors
- β Floor joists search: β₯5 matches
- β Stucco system search: β₯3 matches
- β Railing search: β₯5 matches
- β Stair search: β₯3 matches
- β Bidirectional size conversion validation
Test data stability: Uses minimum thresholds instead of exact counts to handle production data changes.
| Table | Records | Purpose |
|---|---|---|
public.cm_master_materials |
124 | Material family taxonomy (THE BACKBONE) |
public.csi_sections |
6,428 | CSI MasterFormat codes |
public.vendors |
83 | Manufacturer directory |
public.products |
205 | Main product catalog |
public.product_knowledge |
151 | Deep product intelligence |
Each vendor has a dedicated schema with products_enriched table:
boehmers.products_enriched- CMU blocksrichvale.products_enriched- CMU blocksbrampton_brick.products_enriched- CMU blockssto.products_enriched- Stucco/EIFS systemskawneer.products_enriched- Curtain wall systemsboise_cascade.products_enriched- Engineered wooddurock.products_enriched- Stucco systemscentury_railings.products_enriched- Railingsbaros_vision.products_enriched- Glass railings- ... and 3 more
SELECT
product_id,
model_code,
use_when,
key_features, -- JSONB
technical_specs, -- JSONB
performance_data -- JSONB
FROM {vendor_schema}.products_enrichedEdit SynonymExpander.cs:
private static readonly Dictionary<string, string[]> SynonymDictionary = new()
{
["your_term"] = ["synonym1", "synonym2", "synonym3"],
// ...
};The SizeCalculator automatically handles any imperial/metric conversion. To add custom mappings, extend the calculation logic in SizeCalculator.cs.
No code changes needed! The SchemaDiscoveryService automatically detects new vendor schemas at startup if they have a products_enriched table.
- Schema Discovery: ~50ms (cached after first call)
- Material Family Lookup: ~10-30ms
- Product Search: ~50-100ms
- Parallel Vendor Queries: ~100-200ms (12 schemas in parallel)
- Total Search Time: ~200-400ms typical
Scalability:
- Handles 100+ products per search
- Queries 12 vendor schemas in parallel
- Connection pooling via Npgsql
- Async/await throughout for non-blocking I/O
Failed to connect to 54.82.205.23:5432
Solution: Your network blocks port 5432. Use the Session Pooler connection string from Supabase Dashboard.
Solution: Verify the username format:
- Direct connection:
postgres - Session pooler:
postgres.{project_ref}
Possible causes:
- Check if
family_labelwas resolved (look atwarningsin output) - Verify size pattern format (use quotes:
"8 inch"not8 inch) - Enable debug logging:
"LogLevel": { "SourcingEngine": "Debug" }
- Verify connection string in
tests/SourcingEngine.Tests/appsettings.Test.json - Check Supabase project is active (not paused)
- Verify firewall allows outbound connections to Supabase
Extracts structured BOM line items from uploaded documents (PDF, XLSX, CSV) using Amazon Bedrock Nova Pro.
- Trigger:
bom-extraction-queue(via Amazon MQ event source mapping) - Output: Publishes
ExtractionResultMessagetobom.extractionexchange with routing keyextract.result - Model:
us.amazon.nova-pro-v1:0(cross-region inference profile) - Timeout: 180s, 512 MB
Runs the 8-step search pipeline for each BOM line item, finding matching products from the database.
- Trigger:
bom-extraction-result-queue(output of the extraction Lambda) - Output: Splits results into two queues:
sourcing-engine-search-results-queueβ items with β₯1 product matchsourcing-engine-search-zero-results-queueβ items with 0 matches
- Models: Titan Embed Text v2 (embeddings) + Nova Lite v1 (parsing)
- Timeout: 300s, 1024 MB
- Dead Letter: Failed messages go to
sourcing-engine-poison-queuevia DLX policy on the trigger queue
All DTOs live in SourcingEngine.Common/Models/QueueMessages.cs:
| Message | Fields | Queue |
|---|---|---|
ExtractionResultMessage |
traceId, fileName, items[], extractedAt | bom-extraction-result-queue |
SourcingResultMessage |
traceId, fileName, items[] (with products[]) | sourcing-engine-search-results-queue |
SourcingZeroResultsMessage |
traceId, fileName, items[], publishedAt | sourcing-engine-search-zero-results-queue |
All infrastructure is managed in a single CDK stack: infra/BomExtractionLambdaCdk/.
| Resource | Details |
|---|---|
| BOM Extraction Lambda | Docker container image, IAM role (Bedrock + S3 + MQ + Secrets Manager), event source mapping |
| Search Lambda | Docker container image, IAM role (Bedrock + MQ + Secrets Manager), event source mapping |
| RabbitMQ Topology | CDK Custom Resource that calls the RabbitMQ Management API to create exchanges, queues, bindings, and DLX policies |
Since CloudFormation has no native support for RabbitMQ queue/exchange management, we use a Python Lambda Custom Resource (infra/BomExtractionLambdaCdk/lambda/rabbitmq-topology/index.py) that:
- Retrieves broker credentials from Secrets Manager
- Calls the RabbitMQ Management HTTP API (port 443 on Amazon MQ)
- Creates exchanges, queues, and bindings via idempotent PUT/POST calls
- Applies DLX policies to existing queues (non-destructive)
Properties:
- Idempotent β safe to run on every deploy
- Preserves on delete β stack deletion does NOT remove queues/messages
- Conditional β only runs when
brokerSecretArnis provided
| Key | Default | Purpose |
|---|---|---|
brokerArn |
arn:aws:mq:us-east-2:... |
Amazon MQ broker ARN |
brokerHost |
b-24d11402-...on.aws |
Broker hostname for Management API |
brokerSecretArn |
(empty) | Secrets Manager ARN for broker credentials |
vpcId |
(empty) | VPC ID (only if broker is private) |
queueName |
bom-extraction-queue |
BOM extraction trigger queue |
sourcingQueueName |
bom-extraction-result-queue |
Search Lambda trigger queue |
sourcingDbConnectionString |
(empty) | Supabase PostgreSQL connection string |
# BOM Extraction Lambda β build, push to ECR, deploy
./scripts/deploy-lambda.sh --deploy
# Search Lambda β build, push to ECR, deploy
./scripts/deploy-sourcing-lambda.sh --deploy
# CDK only (synth dry-run)
./scripts/deploy-lambda.sh --synth
# Image push only (no CDK deploy)
./scripts/deploy-sourcing-lambda.shBoth scripts will:
- Build a Docker image (
linux/amd64) - Create the ECR repository if it doesn't exist
- Push the image to ECR
- (With
--deploy) Runcdk deployand update the Lambda function code
Each Lambda has a LocalRunner project for F5 debugging in VS Code:
| Launch Config | What it does |
|---|---|
Lambda: Local RabbitMQ Consumer |
Connects to local RabbitMQ, consumes from queue, invokes handler |
Lambda: Replay Event File |
Reads a saved JSON event file and replays it through the handler |
Search Lambda: Local RabbitMQ Consumer |
Same pattern for the Search Lambda |
Search Lambda: Replay Event File |
Replay a saved extraction result through search |
Use Docker Compose or standalone Docker with the definitions file:
docker run -d --name rabbitmq \
-p 5672:5672 -p 15672:15672 \
-v $(pwd)/src/SourcingEngine.BomExtraction.Lambda/local/rabbitmq-definitions.json:/etc/rabbitmq/definitions.json \
-e RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS='-rabbitmq_management load_definitions "/etc/rabbitmq/definitions.json"' \
rabbitmq:3-managementThis creates all exchanges, queues, and bindings locally (matching the production topology).
Internal tool for MVP Partner Package. Not licensed for external use.
This is a proof-of-concept implementation. For production use:
- Add authentication/authorization
- Implement caching (Redis/memory cache)
- Add retry policies for database queries
- Implement rate limiting
- Add comprehensive error handling
- Add API endpoints (REST/GraphQL)
- Add pagination for large result sets