Skip to content

Commit 2b789fa

Browse files
Changes before error encountered
Co-authored-by: delicatacurtis <247246500+delicatacurtis@users.noreply.github.com>
1 parent ad7f2b3 commit 2b789fa

File tree

8 files changed

+843
-20
lines changed

8 files changed

+843
-20
lines changed

.env.example

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,19 @@ RUNNING_MIGRATIONS_AND_SEEDERS=false
104104
# Google Cloud Vision API for Handwriting Recognition
105105
# Get your API key from: https://console.cloud.google.com/apis/credentials
106106
GOOGLE_VISION_API_KEY=
107+
108+
# Genealogy Service API Keys
109+
# MyHeritage API Configuration
110+
MYHERITAGE_API_KEY=
111+
MYHERITAGE_BASE_URL=https://api.myheritage.com/v1
112+
MYHERITAGE_TIMEOUT=30
113+
114+
# Ancestry API Configuration
115+
ANCESTRY_API_KEY=
116+
ANCESTRY_BASE_URL=https://api.ancestry.com/v1
117+
ANCESTRY_TIMEOUT=30
118+
119+
# FamilySearch API Configuration
120+
FAMILYSEARCH_API_KEY=
121+
FAMILYSEARCH_BASE_URL=https://api.familysearch.org/platform
122+
FAMILYSEARCH_TIMEOUT=30

app/Jobs/RunRecordMatchingJob.php

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@
55
use ReflectionClass;
66
use App\Models\Person;
77
use App\Services\RecordMatcher\Providers\ExampleProvider;
8+
use App\Services\RecordMatcher\Providers\MyHeritageProvider;
9+
use App\Services\RecordMatcher\Providers\AncestryProvider;
10+
use App\Services\RecordMatcher\Providers\FamilySearchProvider;
811
use App\Services\RecordMatcher\RecordMatcherService;
912
use Illuminate\Bus\Queueable;
1013
use Illuminate\Contracts\Queue\ShouldQueue;
1114
use Illuminate\Foundation\Bus\Dispatchable;
1215
use Illuminate\Queue\InteractsWithQueue;
1316
use Illuminate\Queue\SerializesModels;
17+
use Illuminate\Support\Facades\Log;
1418

1519
class RunRecordMatchingJob implements ShouldQueue
1620
{
@@ -20,29 +24,75 @@ class RunRecordMatchingJob implements ShouldQueue
2024

2125
public function handle(RecordMatcherService $matcher)
2226
{
23-
// Providers could be defined in config; for now use ExampleProvider
24-
$providers = [
25-
new ExampleProvider(),
26-
// Add real providers via DI/config
27-
];
27+
// Initialize providers based on configuration
28+
$providers = [];
29+
30+
// Add MyHeritage provider if configured
31+
$myHeritage = new MyHeritageProvider();
32+
if ($myHeritage->isConfigured()) {
33+
$providers[] = $myHeritage;
34+
}
35+
36+
// Add Ancestry provider if configured
37+
$ancestry = new AncestryProvider();
38+
if ($ancestry->isConfigured()) {
39+
$providers[] = $ancestry;
40+
}
41+
42+
// Add FamilySearch provider if configured
43+
$familySearch = new FamilySearchProvider();
44+
if ($familySearch->isConfigured()) {
45+
$providers[] = $familySearch;
46+
}
47+
48+
// If no providers configured, use example provider for testing
49+
if (empty($providers)) {
50+
Log::warning('No genealogy providers configured, using example provider');
51+
$providers[] = new ExampleProvider();
52+
}
53+
54+
Log::info('Record matching job started', [
55+
'providers' => array_map(fn($p) => (new ReflectionClass($p))->getShortName(), $providers),
56+
]);
2857

2958
// Fetch a sample of persons to run against (could be queued per-person)
3059
$persons = Person::whereNotNull('last_name')->limit(200)->get();
3160

61+
$totalMatches = 0;
62+
3263
foreach ($persons as $person) {
3364
foreach ($providers as $provider) {
34-
$candidates = $provider->search($person);
35-
$scored = $matcher->scoreCandidates($person, $candidates);
36-
37-
foreach ($scored as $entry) {
38-
$candidate = $entry['candidate'];
39-
$score = $entry['score'];
40-
// Only persist suggestions above a threshold (e.g., 0.45)
41-
if ($score >= config('ai_record_match.min_confidence', 0.45)) {
42-
$matcher->persistSuggestion($person->id, (new ReflectionClass($provider))->getShortName(), $candidate, $score);
65+
try {
66+
$candidates = $provider->search($person);
67+
$scored = $matcher->scoreCandidates($person, $candidates);
68+
69+
foreach ($scored as $entry) {
70+
$candidate = $entry['candidate'];
71+
$score = $entry['score'];
72+
// Only persist suggestions above a threshold (e.g., 0.45)
73+
if ($score >= config('ai_record_match.min_confidence', 0.45)) {
74+
$matcher->persistSuggestion(
75+
$person->id,
76+
(new ReflectionClass($provider))->getShortName(),
77+
$candidate,
78+
$score
79+
);
80+
$totalMatches++;
81+
}
4382
}
83+
} catch (\Exception $e) {
84+
Log::error('Record matching failed for person', [
85+
'person_id' => $person->id,
86+
'provider' => (new ReflectionClass($provider))->getShortName(),
87+
'error' => $e->getMessage(),
88+
]);
4489
}
4590
}
4691
}
92+
93+
Log::info('Record matching job completed', [
94+
'total_matches_found' => $totalMatches,
95+
'persons_processed' => $persons->count(),
96+
]);
4797
}
4898
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
<?php
2+
3+
namespace App\Services\RecordMatcher\Providers;
4+
5+
use App\Models\Person;
6+
use Illuminate\Support\Facades\Http;
7+
use Illuminate\Support\Facades\Log;
8+
use Exception;
9+
10+
/**
11+
* Ancestry provider for searching external genealogy records.
12+
* Integrates with Ancestry API to find potential matches.
13+
*/
14+
class AncestryProvider implements ExternalRecordProviderInterface
15+
{
16+
protected string $apiKey;
17+
protected string $baseUrl;
18+
protected int $timeout;
19+
20+
public function __construct()
21+
{
22+
$this->apiKey = config('services.ancestry.api_key', '');
23+
$this->baseUrl = config('services.ancestry.base_url', 'https://api.ancestry.com/v1');
24+
$this->timeout = config('services.ancestry.timeout', 30);
25+
}
26+
27+
/**
28+
* Search Ancestry for matching records.
29+
*
30+
* @param Person|int $localPerson
31+
* @return array
32+
*/
33+
public function search($localPerson): array
34+
{
35+
$person = is_int($localPerson) ? Person::find($localPerson) : $localPerson;
36+
37+
if (!$person) {
38+
return [];
39+
}
40+
41+
// If API key is not configured, return empty results
42+
if (empty($this->apiKey)) {
43+
Log::warning('Ancestry API key not configured');
44+
return [];
45+
}
46+
47+
try {
48+
$searchParams = $this->buildSearchParams($person);
49+
$response = $this->performSearch($searchParams);
50+
51+
return $this->parseResponse($response);
52+
} catch (Exception $e) {
53+
Log::error('Ancestry search failed', [
54+
'person_id' => $person->id,
55+
'error' => $e->getMessage(),
56+
]);
57+
return [];
58+
}
59+
}
60+
61+
/**
62+
* Build search parameters from person data.
63+
*
64+
* @param Person $person
65+
* @return array
66+
*/
67+
protected function buildSearchParams(Person $person): array
68+
{
69+
$params = [];
70+
71+
// Name parameters
72+
if ($person->first_name) {
73+
$params['givenName'] = $person->first_name;
74+
}
75+
if ($person->last_name) {
76+
$params['surname'] = $person->last_name;
77+
}
78+
79+
// Birth information
80+
if ($person->birthday) {
81+
$params['birthYear'] = $person->birthday->format('Y');
82+
}
83+
84+
if ($person->birthplace) {
85+
$params['birthLocation'] = $person->birthplace->place ?? null;
86+
}
87+
88+
// Death information
89+
if ($person->deathday) {
90+
$params['deathYear'] = $person->deathday->format('Y');
91+
}
92+
93+
if ($person->deathplace) {
94+
$params['deathLocation'] = $person->deathplace->place ?? null;
95+
}
96+
97+
// Gender
98+
if ($person->sex) {
99+
$params['gender'] = $person->sex;
100+
}
101+
102+
return array_filter($params);
103+
}
104+
105+
/**
106+
* Perform the actual API search.
107+
*
108+
* @param array $searchParams
109+
* @return array
110+
*/
111+
protected function performSearch(array $searchParams): array
112+
{
113+
$response = Http::timeout($this->timeout)
114+
->withHeaders([
115+
'Authorization' => 'Bearer ' . $this->apiKey,
116+
'Accept' => 'application/json',
117+
])
118+
->get($this->baseUrl . '/search/records', $searchParams);
119+
120+
if (!$response->successful()) {
121+
throw new Exception('Ancestry API request failed: ' . $response->status());
122+
}
123+
124+
return $response->json() ?? [];
125+
}
126+
127+
/**
128+
* Parse API response into standardized format.
129+
*
130+
* @param array $response
131+
* @return array
132+
*/
133+
protected function parseResponse(array $response): array
134+
{
135+
$results = [];
136+
$records = $response['records'] ?? $response['searchResults'] ?? [];
137+
138+
foreach ($records as $record) {
139+
$person = $record['person'] ?? $record;
140+
141+
$results[] = [
142+
'id' => $person['id'] ?? $person['personId'] ?? null,
143+
'external_id' => $person['id'] ?? $person['personId'] ?? null,
144+
'tree_id' => $person['treeId'] ?? null,
145+
'first_name' => $person['givenName'] ?? $person['firstName'] ?? '',
146+
'last_name' => $person['surname'] ?? $person['lastName'] ?? '',
147+
'birth_year' => $person['birthYear'] ?? null,
148+
'birth_date' => $person['birthDate'] ?? null,
149+
'birth_place' => $person['birthLocation'] ?? $person['birthPlace'] ?? null,
150+
'death_year' => $person['deathYear'] ?? null,
151+
'death_date' => $person['deathDate'] ?? null,
152+
'death_place' => $person['deathLocation'] ?? $person['deathPlace'] ?? null,
153+
'gender' => $person['gender'] ?? $person['sex'] ?? null,
154+
'parents' => $person['parents'] ?? null,
155+
'spouse' => $person['spouse'] ?? null,
156+
'children' => $person['children'] ?? [],
157+
'source_url' => $person['recordUrl'] ?? $person['url'] ?? null,
158+
'tree_name' => $person['treeName'] ?? null,
159+
'tree_owner' => $person['treeOwner'] ?? null,
160+
];
161+
}
162+
163+
return $results;
164+
}
165+
166+
/**
167+
* Get provider name.
168+
*
169+
* @return string
170+
*/
171+
public function getName(): string
172+
{
173+
return 'Ancestry';
174+
}
175+
176+
/**
177+
* Check if provider is configured.
178+
*
179+
* @return bool
180+
*/
181+
public function isConfigured(): bool
182+
{
183+
return !empty($this->apiKey);
184+
}
185+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace App\Services\RecordMatcher\Providers;
4+
5+
use App\Models\Person;
6+
7+
/**
8+
* Example provider for testing the record matching system.
9+
* This returns sample data for demonstration purposes.
10+
*/
11+
class ExampleProvider implements ExternalRecordProviderInterface
12+
{
13+
/**
14+
* Search for matching records in the example data source.
15+
*
16+
* @param Person|int $localPerson
17+
* @return array
18+
*/
19+
public function search($localPerson): array
20+
{
21+
$person = is_int($localPerson) ? Person::find($localPerson) : $localPerson;
22+
23+
if (!$person) {
24+
return [];
25+
}
26+
27+
// Return empty array for now - this is just a placeholder
28+
// Real implementation would search an actual data source
29+
return [];
30+
}
31+
}

0 commit comments

Comments
 (0)