Comprehensive guide for setting up, developing, testing, and deploying the ERE Prototype Template.
For architecture overview, see architecture.md. For API flows, see sequence-diagrams.md.
| Tool | Version | Purpose |
|---|---|---|
| Python | 3.12+ | Runtime |
| Poetry | 1.7+ | Dependency management |
| Docker | 24+ | Container runtime |
| Docker Compose | 2.20+ | Multi-service orchestration |
| Redis | 7.x | Messaging transport (or Docker) |
| Make | any | Build automation |
# Clone and enter the project
git clone <repository-url>
cd ere-prototype-template
# Install dependencies (ERE service)
make install
# Install client dependencies
cd ere-client && make install && cd ..
# Run tests (no external services needed)
make test
# Start with Docker (Redis + ERE)
make docker-up# Install base dependencies
poetry install
# Install with MongoDB support
poetry install --extras mongodb
# Install with PostgreSQL support
poetry install --extras postgres
# Install with embeddings support (large download: PyTorch/ONNX)
poetry install --extras embeddings
# Install everything
poetry install --all-extrascd ere-client
poetry installThe client has minimal dependencies: click, redis, rich, pydantic, PyYAML.
Configuration is loaded from YAML with environment variable overrides:
# config/ere_config.yaml
service:
name: ere-prototype
version: "0.1.0"
supported_entity_types:
- "ORGANIZATION"
- "PERSON"
messaging:
backend: redis # "in_memory" or "redis"
redis:
host: localhost
port: 6379
request_channel: "ere_requests"
response_channel: "ere_responses"
storage:
backend: in_memory # "in_memory", "mongodb", or "postgres"
clustering:
similarity_algorithm: cosine # "cosine", "jaccard", or "levenshtein"
similarity_threshold: 0.75
confidence_threshold: 0.5
max_candidates: 5
# Quality enhancements
identifier_exact_match_boost: 0.30
identifier_mismatch_penalty: 0.02
representative_update_threshold: 0.90
blocking_enabled: false
embedding:
enabled: true
model_name: "sentence-transformers/all-MiniLM-L6-v2"
limits:
workers: 4 # Concurrent worker threads (1 = sequential)
logging:
level: INFO
traffic_log_payloads: true| Variable | Overrides | Example |
|---|---|---|
ERE_CONFIG |
Config file path | config/ere_config_docker.yaml |
ERE_REDIS_HOST |
messaging.redis.host |
redis |
ERE_REDIS_PORT |
messaging.redis.port |
6379 |
ERE_REDIS_DB |
messaging.redis.db |
0 |
ERE_REDIS_PASSWORD |
messaging.redis.password |
secretpass |
ERE_MONGODB_URI |
storage.mongodb.uri |
mongodb://mongo:27017 |
ERE_MONGODB_DATABASE |
storage.mongodb.database |
ere_prod |
# ere-client/config/client_config.yaml
redis:
host: localhost
port: 6379
request_channel: "ere_requests"
response_channel: "ere_responses"
defaults:
source_id: "ere-client"
entity_type: "ORGANIZATION"
content_type: "text/turtle"
response:
timeout: 30
poll_interval: 1No configuration needed. Data is lost on restart. Best for:
- Unit and integration testing
- Quick experimentation
- Development without Docker
storage:
backend: mongodb
mongodb:
uri: "mongodb://ere:ere_dev_password@mongodb:27017/ere_prototype?authSource=admin&directConnection=true"
database: "ere_prototype"
entities_collection: "entities"
clusters_collection: "clusters"Requirements:
- MongoDB 8.x running (or Docker:
make docker-up-mongodb) pymongoinstalled (poetry install --extras mongodb)- For vector search: MongoDB Atlas or self-managed with Atlas Search (mongot)
storage:
backend: postgres
postgres:
host: localhost
port: 5432
database: ere_prototype
user: ere
password: ""
vector_dimension: 384Requirements:
- PostgreSQL 15+ with pgvector extension
- Docker:
make docker-up-postgres psycopg[binary]installed (poetry install --extras postgres)
make test # Run all unit tests
make test-verbose # Verbose output with test names
make coverage # Run with coverage reportUnit tests use InMemory backends — no external services required.
make test-bdd # Run Behave BDD scenariosBDD tests describe resolution behaviour in Gherkin syntax.
make test-docker # Run tests against Docker servicesStarts Docker Compose services, runs integration tests, then stops services.
make test-all # Unit + BDD + Docker testsmake lint # Run ruff linter
make format # Auto-format with ruff
make lint-fix # Auto-fix linting issues# Redis only (messaging)
docker compose --profile redis up -d
# Redis + MongoDB
docker compose --profile redis --profile mongodb up -d
# Redis + PostgreSQL
docker compose --profile redis --profile postgres up -d
# Full stack with monitoring
docker compose --profile redis --profile mongodb --profile postgres --profile redis-insight up -dmake docker-up # Core + Redis (default)
make docker-up-mongodb # Core + Redis + MongoDB + Mongo Express
make docker-up-postgres # Core + Redis + PostgreSQL + pgAdmin
make docker-down # Stop all services
make docker-logs # Follow service logs
make docker-build # Rebuild ERE service image| UI | URL | Credentials |
|---|---|---|
| Mongo Express | http://localhost:8081 | (none) |
| pgAdmin | http://localhost:5050 | admin@admin.com / admin |
| Redis Insight | http://localhost:5540 | (none) |
# With defaults (InMemory storage, InMemory messaging)
python -m ere.main
# With specific config
python -m ere.main --config config/ere_config.yaml
# With Docker Redis
ERE_REDIS_HOST=localhost python -m ere.main --config config/ere_config.yamlmake docker-up # Starts ERE + Redis
make docker-logs # View logscd ere-client
# Send a resolution request
poetry run ere-client resolve "ACME Corporation, London, UK"
# Send from a JSON file
poetry run ere-client send-file samples/sample_request.json
# Monitor responses
poetry run ere-client listen
# Check connection status
poetry run ere-client statusredis.exceptions.ConnectionError: Error connecting to localhost:6379
Solution: Start Redis via Docker (make docker-up) or install Redis locally.
pymongo.errors.ServerSelectionTimeoutError: localhost:27017
Solution: Start MongoDB (make docker-up-mongodb) and verify port 27017 is accessible.
psycopg.errors.UndefinedFile: could not open extension control file "vector"
Solution: Use the pgvector/pgvector:pg15 Docker image which includes the extension pre-installed.
ImportError: No module named 'sentence_transformers'
Solution: Install the embeddings extra: poetry install --extras embeddings. Note: this downloads ~2GB of PyTorch dependencies.
ValueError: Embedding dimension 768 doesn't match configured dimension 384
Solution: Ensure embedding.model_name in config matches the vector_dimension in storage config. The default model (all-MiniLM-L6-v2) produces 384-dimensional vectors.
⏱ No response received within timeout.
Solution: Verify the ERE service is running and consuming from the same Redis channel. Check ere-client status to confirm connectivity and queue lengths.
The Property Extraction feature enables structured RDF/TTL property extraction for entity resolution. Instead of embedding an entity's entire serialized content as a single string, the system can parse RDF/TTL content, extract individual properties, apply text preprocessing, embed each property separately, and combine them into a weighted composite vector.
This improves resolution accuracy by allowing administrators to control which properties matter most and how they are normalized before comparison.
Property extraction is configured in config/ere_config.yaml under the property_extraction key. The structure maps entity type URIs to lists of property definitions.
property_extraction:
"http://www.w3.org/ns/org#Organization":
- name: "legal_name"
xpath: "org:Organization/org:legalName"
weight: 2.0
filter: "lowercase"
- name: "alternative_name"
xpath: "org:Organization/skos:altLabel"
weight: 1.0
filter: "trim"
- name: "identifier"
xpath: "org:Organization/org:identifier"
weight: 3.0
filter: "remove_special_characters"property_extraction:
"http://xmlns.com/foaf/0.1/Person":
- name: "full_name"
xpath: "foaf:Person/foaf:name"
weight: 3.0
filter: "lowercase"
- name: "email"
xpath: "foaf:Person/foaf:mbox"
weight: 2.5
filter: "lowercase"
- name: "family_name"
xpath: "foaf:Person/foaf:familyName"
weight: 2.0
filter: "trim"
- name: "given_name"
xpath: "foaf:Person/foaf:givenName"
weight: 1.5
filter: "trim"property_extraction:
"http://www.w3.org/2004/02/skos/core#Concept":
- name: "preferred_label"
xpath: "skos:Concept/skos:prefLabel"
weight: 3.0
filter: "lowercase"
- name: "alt_labels"
xpath: "skos:Concept/skos:altLabel"
weight: 1.5
filter: "trim"
- name: "definition"
xpath: "skos:Concept/skos:definition"
weight: 1.0
filter: "trim"You can configure different entity types in the same file:
property_extraction:
"http://www.w3.org/ns/org#Organization":
- name: "legal_name"
xpath: "org:Organization/org:legalName"
weight: 2.0
filter: "lowercase"
"http://xmlns.com/foaf/0.1/Person":
- name: "full_name"
xpath: "foaf:Person/foaf:name"
weight: 3.0
filter: "lowercase"
- name: "email"
xpath: "foaf:Person/foaf:mbox"
weight: 2.0
filter: "lowercase"If you need to reference an ontology not covered by built-in prefixes, use full URIs in angle brackets:
property_extraction:
"<http://example.org/ontology#CustomEntity>":
- name: "custom_label"
xpath: "<http://example.org/ontology#CustomEntity>/<http://example.org/ontology#label>"
weight: 2.0
filter: "trim"| Field | Type | Constraints | Description |
|---|---|---|---|
name |
string | Non-empty, max 128 chars, unique within entity type | Human-readable property identifier |
xpath |
string | Non-empty, max 512 chars | XPath selector to locate the property in RDF |
weight |
float | 0.0 ≤ weight ≤ 10.0 | Importance multiplier for the composite vector (0 = extract-only, no embedding) |
filter |
string or list | One or more of the supported filters | Preprocessing transformation(s) applied in sequence |
Supported filters: uppercase, lowercase, trim, remove_special_characters, strip_company_suffixes, strip_telephone_chars, numeric, strip_dots, strip_spaces, alphanumeric, last_part_of_url
The xpath field uses a simplified path notation that maps RDF types and properties to SPARQL queries internally. The format locates subjects of a specific rdf:type and extracts a specific property value from those subjects.
<type_selector>/<property_selector>
Where each part is either a prefixed name or a full URI in angle brackets.
prefix:Type/prefix:property
This translates internally to the SPARQL query:
SELECT ?value WHERE {
?s a <expanded_type_uri> .
?s <expanded_property_uri> ?value .
}Example: org:Organization/org:legalName queries for all subjects with rdf:type org:Organization and extracts their org:legalName values.
<http://full-type-uri>/<http://full-property-uri>
Example: <http://example.org/ns#Company>/<http://example.org/ns#tradeName> queries for subjects typed as http://example.org/ns#Company with the property http://example.org/ns#tradeName.
| Prefix | Namespace URI |
|---|---|
org |
http://www.w3.org/ns/org# |
foaf |
http://xmlns.com/foaf/0.1/ |
skos |
http://www.w3.org/2004/02/skos/core# |
rdfs |
http://www.w3.org/2000/01/rdf-schema# |
rdf |
http://www.w3.org/1999/02/22-rdf-syntax-ns# |
dc |
http://purl.org/dc/elements/1.1/ |
dct |
http://purl.org/dc/terms/ |
schema |
http://schema.org/ |
vcard |
http://www.w3.org/2006/vcard/ns# |
owl |
http://www.w3.org/2002/07/owl# |
xsd |
http://www.w3.org/2001/XMLSchema# |
| XPath Selector | What it Extracts |
|---|---|
org:Organization/org:legalName |
Legal name of organizations |
org:Organization/skos:altLabel |
Alternative labels of organizations |
foaf:Person/foaf:name |
Full name of persons |
foaf:Person/foaf:mbox |
Email addresses of persons |
skos:Concept/skos:prefLabel |
Preferred labels of SKOS concepts |
rdfs:Resource/rdfs:label |
Labels of any RDFS resource |
schema:Organization/schema:name |
Schema.org organization names |
<http://example.org/ns#Entity>/<http://example.org/ns#id> |
Custom property via full URIs |
When a property has multiple values (e.g., multiple skos:altLabel entries), all matching values are:
- Sorted in lexicographic (Unicode code point) ascending order
- Concatenated with a single space separator
- Truncated to 10,000 characters if the concatenated result exceeds that limit
Each property definition includes a filter that normalizes the extracted text before embedding. Filters are applied after property extraction and multi-value concatenation.
Converts all characters to uppercase using Python's str.upper().
| Input | Output |
|---|---|
"Acme Corporation" |
"ACME CORPORATION" |
"café résumé" |
"CAFÉ RÉSUMÉ" |
"hello world 123" |
"HELLO WORLD 123" |
"Already UPPER" |
"ALREADY UPPER" |
Converts all characters to lowercase using Python's str.lower().
| Input | Output |
|---|---|
"ACME Corporation" |
"acme corporation" |
"John DOE" |
"john doe" |
"Mixed Case 123" |
"mixed case 123" |
"already lower" |
"already lower" |
Removes all leading and trailing Unicode whitespace characters (spaces, tabs, newlines, and all Unicode whitespace categories).
| Input | Output |
|---|---|
" hello world " |
"hello world" |
"\t\n Acme Corp \n" |
"Acme Corp" |
" multiple spaces inside " |
"multiple spaces inside" |
"no_whitespace" |
"no_whitespace" |
Note: trim only removes leading/trailing whitespace. Interior whitespace is preserved.
Applies a three-step normalization process:
- NFKD Unicode normalization — decomposes accented characters (e.g.,
é→e+ combining accent) - Discard non-ASCII characters — removes anything that isn't in the 0–127 ASCII range
- Remove non-alphanumeric non-whitespace — strips remaining punctuation, symbols, etc.
| Input | Output |
|---|---|
"Café Résumé" |
"Cafe Resume" |
"O'Brien & Associates, Ltd." |
"OBrien Associates Ltd" |
"user@example.com" |
"userexamplecom" |
"hello-world_123!" |
"helloworld123" |
"naïve coöperation" |
"naive cooperation" |
"100% complete™" |
"100 completeTM" |
"日本語テスト" |
"" (empty — all non-ASCII) |
- Filters are applied in sequence as specified in the
filterlist. - If the value is empty after filtering, that property is excluded from the composite vector computation entirely.
- Filters operate on the already-concatenated multi-value string (after join with space).
- The
last_part_of_urlfilter is a stop filter: if it transforms the value (detects a URL), subsequent filters in the chain are skipped. - Properties with
weight: 0.0are extracted for theparsed_representationJSON but are not embedded or included in the composite vector.
The full property extraction pipeline operates in this order for each entity mention with content_type = "text/turtle":
- Config lookup — Find property definitions matching the entity's
entity_typeURI - Parse — Parse the TTL content string into an RDF graph using
rdflib - Extract — For each configured property, query the graph using the XPath selector
- Concatenate — Sort multi-value results lexicographically, join with single space
- Truncate — If concatenated value exceeds 10,000 characters, truncate
- Preprocess — Apply the configured filter to normalize the text
- Embed — Pass non-empty preprocessed values to the HuggingFace embedder
- Weight — Multiply each embedding vector by its configured weight
- Combine — Sum all weighted vectors and L2-normalize to unit length
Fallback conditions — The system falls back to full-content embedding when:
- No
property_extractionconfig exists for the entity type - TTL content cannot be parsed (malformed Turtle)
- All properties yield empty values after extraction and preprocessing
- The embedder is disabled in configuration