Skip to content

Commit 6cc202c

Browse files
committed
add exif extraction, virtual folders
1 parent dd4e390 commit 6cc202c

File tree

11 files changed

+1077
-80
lines changed

11 files changed

+1077
-80
lines changed

README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@
1616

1717
# dsearch
1818

19-
A configurable filesystem search service powered by [bleve](https://github.com/blevesearch/bleve) for GNU/Linux.
19+
A configurable filesystem search service powered by [bleve](https://github.com/blevesearch/bleve) for Unix-based operating systems.
2020

2121
## Features
2222

2323
- **Indexing** - Indexes all files in specified paths
2424
- **Configuration** - Configure exclusions, depth, etc.
2525
- **Fuzzy search** - Typo-tolerant search queries
2626
- **Text content extraction** - Optionally extracts and indexes content from text-based files
27+
- **EXIF metadata** - Extracts and indexes EXIF data from image files for advanced search
28+
- **Virtual folders** - Search within specific directories
2729
- **Concurrent indexing** - Multi-worker parallel indexing for fast performance
2830
- **Real-time updates** - File watcher for incremental index updates
2931

@@ -62,6 +64,32 @@ dsearch search "typo" --fuzzy
6264
dsearch search "*" --sort mtime --desc
6365
# Scores content higher than filenames
6466
dsearch search --field body mountain
67+
68+
# Virtual folder search
69+
dsearch search "*" --folder /home/user/Pictures
70+
71+
# EXIF metadata search and filtering
72+
dsearch search "*" --ext .jpg --exif-make Canon
73+
dsearch search "*" --exif-model "Canon EOS 5D"
74+
dsearch search "*" --folder /home/user/Photos --exif-make Nikon
75+
76+
# Sort photos by date taken (newest first)
77+
dsearch search "*" --ext .jpg --sort exif_date --desc
78+
79+
# Find photos by ISO range (low-light shots)
80+
dsearch search "*" --exif-min-iso 1600 --exif-max-iso 6400
81+
82+
# Find portrait shots (focal length range)
83+
dsearch search "*" --exif-min-focal-len 50 --exif-max-focal-len 135
84+
85+
# Find wide aperture shots (shallow depth of field)
86+
dsearch search "*" --exif-min-aperture 1.4 --exif-max-aperture 2.8
87+
88+
# Photos taken in date range
89+
dsearch search "*" --exif-date-after "2024-06-01T00:00:00Z" --exif-date-before "2024-08-31T23:59:59Z"
90+
91+
# GPS location filtering (bounding box)
92+
dsearch search "*" --exif-lat-min 40.0 --exif-lat-max 41.0 --exif-lon-min -74.0 --exif-lon-max -73.0
6593
```
6694

6795
For all options:

cmd/dsearch/main.go

Lines changed: 88 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,30 @@ var (
3434
httpOnly bool
3535
socketOnly bool
3636

37-
searchLimit int
38-
searchField string
39-
searchExt string
40-
searchFuzzy bool
41-
searchSort string
42-
searchSortDesc bool
43-
searchMinSize int64
44-
searchMaxSize int64
45-
searchJSON bool
37+
searchLimit int
38+
searchField string
39+
searchExt string
40+
searchFuzzy bool
41+
searchSort string
42+
searchSortDesc bool
43+
searchMinSize int64
44+
searchMaxSize int64
45+
searchJSON bool
46+
searchFolder string
47+
searchExifMake string
48+
searchExifModel string
49+
searchExifDateAfter string
50+
searchExifDateBefore string
51+
searchExifMinISO int
52+
searchExifMaxISO int
53+
searchExifMinAp float64
54+
searchExifMaxAp float64
55+
searchExifMinFocalLen float64
56+
searchExifMaxFocalLen float64
57+
searchExifLatMin float64
58+
searchExifLatMax float64
59+
searchExifLonMin float64
60+
searchExifLonMax float64
4661
)
4762

4863
var rootCmd = &cobra.Command{
@@ -138,11 +153,26 @@ func init() {
138153
searchCmd.Flags().StringVarP(&searchField, "field", "f", "", "search specific field (filename, body, title)")
139154
searchCmd.Flags().StringVarP(&searchExt, "ext", "e", "", "filter by file extension (e.g., .jpg)")
140155
searchCmd.Flags().BoolVar(&searchFuzzy, "fuzzy", false, "enable fuzzy matching for typos")
141-
searchCmd.Flags().StringVar(&searchSort, "sort", "score", "sort by: score, mtime, size, filename")
156+
searchCmd.Flags().StringVar(&searchSort, "sort", "score", "sort by: score, mtime, size, filename, exif_date, iso, focal_length, aperture")
142157
searchCmd.Flags().BoolVar(&searchSortDesc, "desc", true, "sort descending")
143158
searchCmd.Flags().Int64Var(&searchMinSize, "min-size", 0, "minimum file size in bytes")
144159
searchCmd.Flags().Int64Var(&searchMaxSize, "max-size", 0, "maximum file size in bytes")
145160
searchCmd.Flags().BoolVar(&searchJSON, "json", false, "output results in JSON format")
161+
searchCmd.Flags().StringVar(&searchFolder, "folder", "", "filter by folder path (e.g., /home/user/Pictures)")
162+
searchCmd.Flags().StringVar(&searchExifMake, "exif-make", "", "filter by camera make (e.g., Canon)")
163+
searchCmd.Flags().StringVar(&searchExifModel, "exif-model", "", "filter by camera model (e.g., Canon EOS 5D)")
164+
searchCmd.Flags().StringVar(&searchExifDateAfter, "exif-date-after", "", "photos taken after date (RFC3339 or EXIF format)")
165+
searchCmd.Flags().StringVar(&searchExifDateBefore, "exif-date-before", "", "photos taken before date (RFC3339 or EXIF format)")
166+
searchCmd.Flags().IntVar(&searchExifMinISO, "exif-min-iso", 0, "minimum ISO value")
167+
searchCmd.Flags().IntVar(&searchExifMaxISO, "exif-max-iso", 0, "maximum ISO value")
168+
searchCmd.Flags().Float64Var(&searchExifMinAp, "exif-min-aperture", 0, "minimum aperture (f-number)")
169+
searchCmd.Flags().Float64Var(&searchExifMaxAp, "exif-max-aperture", 0, "maximum aperture (f-number)")
170+
searchCmd.Flags().Float64Var(&searchExifMinFocalLen, "exif-min-focal-len", 0, "minimum focal length in mm")
171+
searchCmd.Flags().Float64Var(&searchExifMaxFocalLen, "exif-max-focal-len", 0, "maximum focal length in mm")
172+
searchCmd.Flags().Float64Var(&searchExifLatMin, "exif-lat-min", 0, "minimum GPS latitude")
173+
searchCmd.Flags().Float64Var(&searchExifLatMax, "exif-lat-max", 0, "maximum GPS latitude")
174+
searchCmd.Flags().Float64Var(&searchExifLonMin, "exif-lon-min", 0, "minimum GPS longitude")
175+
searchCmd.Flags().Float64Var(&searchExifLonMax, "exif-lon-max", 0, "maximum GPS longitude")
146176

147177
indexCmd.AddCommand(indexGenerateCmd)
148178
indexCmd.AddCommand(indexSyncCmd)
@@ -304,15 +334,30 @@ func runSearch(cmd *cobra.Command, args []string) error {
304334
}
305335

306336
clientOpts := &client.SearchOptions{
307-
Query: query,
308-
Limit: limit,
309-
Field: searchField,
310-
Extension: searchExt,
311-
Fuzzy: searchFuzzy,
312-
SortBy: searchSort,
313-
SortDesc: searchSortDesc,
314-
MinSize: searchMinSize,
315-
MaxSize: searchMaxSize,
337+
Query: query,
338+
Limit: limit,
339+
Field: searchField,
340+
Extension: searchExt,
341+
Fuzzy: searchFuzzy,
342+
SortBy: searchSort,
343+
SortDesc: searchSortDesc,
344+
MinSize: searchMinSize,
345+
MaxSize: searchMaxSize,
346+
Folder: searchFolder,
347+
ExifMake: searchExifMake,
348+
ExifModel: searchExifModel,
349+
ExifDateAfter: searchExifDateAfter,
350+
ExifDateBefore: searchExifDateBefore,
351+
ExifMinISO: searchExifMinISO,
352+
ExifMaxISO: searchExifMaxISO,
353+
ExifMinAperture: searchExifMinAp,
354+
ExifMaxAperture: searchExifMaxAp,
355+
ExifMinFocalLen: searchExifMinFocalLen,
356+
ExifMaxFocalLen: searchExifMaxFocalLen,
357+
ExifLatMin: searchExifLatMin,
358+
ExifLatMax: searchExifLatMax,
359+
ExifLonMin: searchExifLonMin,
360+
ExifLonMax: searchExifLonMax,
316361
}
317362

318363
result, err := client.SearchWithOptions(clientOpts)
@@ -348,15 +393,30 @@ func runSearch(cmd *cobra.Command, args []string) error {
348393
}
349394

350395
indexerOpts := &indexer.SearchOptions{
351-
Query: query,
352-
Limit: limit,
353-
Field: searchField,
354-
Extension: searchExt,
355-
Fuzzy: searchFuzzy,
356-
SortBy: searchSort,
357-
SortDesc: searchSortDesc,
358-
MinSize: searchMinSize,
359-
MaxSize: searchMaxSize,
396+
Query: query,
397+
Limit: limit,
398+
Field: searchField,
399+
Extension: searchExt,
400+
Fuzzy: searchFuzzy,
401+
SortBy: searchSort,
402+
SortDesc: searchSortDesc,
403+
MinSize: searchMinSize,
404+
MaxSize: searchMaxSize,
405+
Folder: searchFolder,
406+
ExifMake: searchExifMake,
407+
ExifModel: searchExifModel,
408+
ExifDateAfter: searchExifDateAfter,
409+
ExifDateBefore: searchExifDateBefore,
410+
ExifMinISO: searchExifMinISO,
411+
ExifMaxISO: searchExifMaxISO,
412+
ExifMinAperture: searchExifMinAp,
413+
ExifMaxAperture: searchExifMaxAp,
414+
ExifMinFocalLen: searchExifMinFocalLen,
415+
ExifMaxFocalLen: searchExifMaxFocalLen,
416+
ExifLatMin: searchExifLatMin,
417+
ExifLatMax: searchExifLatMax,
418+
ExifLonMin: searchExifLonMin,
419+
ExifLonMax: searchExifLonMax,
360420
}
361421

362422
result, err = idx.SearchWithOptions(indexerOpts)

config.example.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ text_extensions = [
3636
path = "/home/brandon"
3737
max_depth = 6 # Default depth for home directory
3838
exclude_hidden = true
39+
extract_exif = true # Extract EXIF metadata from images
3940
exclude_dirs = [
4041
"node_modules", "bower_components", "__pycache__", "site-packages",
4142
"venv", ".venv", "target", "dist", "build", "vendor", ".cache"
@@ -45,6 +46,7 @@ exclude_dirs = [
4546
path = "/home/brandon/repos"
4647
max_depth = 6 # Go deeper for repos directory
4748
exclude_hidden = true
49+
extract_exif = false # Skip EXIF extraction for code repos
4850
exclude_dirs = [
4951
"node_modules", "bower_components", "__pycache__", "site-packages",
5052
"venv", ".venv", "target", "dist", "build", "vendor", ".cache",
@@ -55,4 +57,5 @@ exclude_dirs = [
5557
path = "/home/brandon/Documents"
5658
max_depth = 0 # Unlimited depth for Documents
5759
exclude_hidden = false # Include hidden files in Documents
60+
extract_exif = true # Extract EXIF metadata from images
5861
exclude_dirs = []

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/danielgtaylor/huma/v2 v2.34.1
1111
github.com/fsnotify/fsnotify v1.9.0
1212
github.com/go-chi/chi/v5 v5.2.3
13+
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd
1314
github.com/spf13/cobra v1.10.1
1415
)
1516

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
9999
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
100100
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
101101
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
102+
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
103+
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
102104
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
103105
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
104106
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=

internal/api/handlers.go

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,33 @@ type Server struct {
3030
}
3131

3232
type SearchInput struct {
33-
Query string `query:"q" minLength:"1" doc:"Search query" example:"mountain"`
34-
Limit int `query:"limit" default:"10" minimum:"1" maximum:"500" doc:"Maximum results"`
35-
Field string `query:"field" enum:"filename,body" doc:"Search specific field (empty for all)"`
36-
ContentType string `query:"content_type" doc:"Filter by MIME type" example:"image/jpeg"`
37-
Extension string `query:"ext" doc:"Filter by file extension" example:".jpg"`
38-
Fuzzy bool `query:"fuzzy" doc:"Enable fuzzy matching for typos"`
39-
SortBy string `query:"sort" enum:"score,mtime,size,filename," default:"score" doc:"Sort results by field"`
40-
SortDesc bool `query:"desc" default:"true" doc:"Sort descending"`
41-
MinSize int64 `query:"min_size" doc:"Minimum file size in bytes"`
42-
MaxSize int64 `query:"max_size" doc:"Maximum file size in bytes"`
43-
ModifiedAfter string `query:"modified_after" doc:"Filter by modification date (RFC3339)" example:"2024-01-01T00:00:00Z"`
44-
Facets []string `query:"facets" doc:"Enable facets for fields" example:"content_type"`
33+
Query string `query:"q" minLength:"1" doc:"Search query" example:"mountain"`
34+
Limit int `query:"limit" default:"10" minimum:"1" maximum:"500" doc:"Maximum results"`
35+
Field string `query:"field" enum:"filename,body" doc:"Search specific field (empty for all)"`
36+
ContentType string `query:"content_type" doc:"Filter by MIME type" example:"image/jpeg"`
37+
Extension string `query:"ext" doc:"Filter by file extension" example:".jpg"`
38+
Fuzzy bool `query:"fuzzy" doc:"Enable fuzzy matching for typos"`
39+
SortBy string `query:"sort" enum:"score,mtime,size,filename,exif_date,iso,focal_length,aperture," default:"score" doc:"Sort results by field"`
40+
SortDesc bool `query:"desc" default:"true" doc:"Sort descending"`
41+
MinSize int64 `query:"min_size" doc:"Minimum file size in bytes"`
42+
MaxSize int64 `query:"max_size" doc:"Maximum file size in bytes"`
43+
ModifiedAfter string `query:"modified_after" doc:"Filter by modification date (RFC3339)" example:"2024-01-01T00:00:00Z"`
44+
Facets []string `query:"facets" doc:"Enable facets for fields" example:"content_type"`
45+
Folder string `query:"folder" doc:"Filter by folder path" example:"/home/user/Pictures"`
46+
ExifMake string `query:"exif_make" doc:"Filter by camera make" example:"Canon"`
47+
ExifModel string `query:"exif_model" doc:"Filter by camera model" example:"Canon EOS 5D"`
48+
ExifDateAfter string `query:"exif_date_after" doc:"Photos taken after date" example:"2024-01-01T00:00:00Z"`
49+
ExifDateBefore string `query:"exif_date_before" doc:"Photos taken before date" example:"2024-12-31T23:59:59Z"`
50+
ExifMinISO int `query:"exif_min_iso" doc:"Minimum ISO value" example:"100"`
51+
ExifMaxISO int `query:"exif_max_iso" doc:"Maximum ISO value" example:"3200"`
52+
ExifMinAperture float64 `query:"exif_min_aperture" doc:"Minimum aperture (f-number)" example:"1.8"`
53+
ExifMaxAperture float64 `query:"exif_max_aperture" doc:"Maximum aperture (f-number)" example:"16"`
54+
ExifMinFocalLen float64 `query:"exif_min_focal_len" doc:"Minimum focal length in mm" example:"24"`
55+
ExifMaxFocalLen float64 `query:"exif_max_focal_len" doc:"Maximum focal length in mm" example:"200"`
56+
ExifLatMin float64 `query:"exif_lat_min" doc:"Minimum GPS latitude" example:"40.0"`
57+
ExifLatMax float64 `query:"exif_lat_max" doc:"Maximum GPS latitude" example:"41.0"`
58+
ExifLonMin float64 `query:"exif_lon_min" doc:"Minimum GPS longitude" example:"-74.0"`
59+
ExifLonMax float64 `query:"exif_lon_max" doc:"Maximum GPS longitude" example:"-73.0"`
4560
}
4661

4762
type SearchOutput struct {
@@ -80,18 +95,33 @@ func RegisterHandlers(srv *Server, api huma.API) {
8095
Tags: []string{"Search"},
8196
}, func(ctx context.Context, input *SearchInput) (*SearchOutput, error) {
8297
opts := &indexer.SearchOptions{
83-
Query: input.Query,
84-
Limit: input.Limit,
85-
Field: input.Field,
86-
ContentType: input.ContentType,
87-
Extension: input.Extension,
88-
Fuzzy: input.Fuzzy,
89-
SortBy: input.SortBy,
90-
SortDesc: input.SortDesc,
91-
MinSize: input.MinSize,
92-
MaxSize: input.MaxSize,
93-
ModifiedAfter: input.ModifiedAfter,
94-
Facets: input.Facets,
98+
Query: input.Query,
99+
Limit: input.Limit,
100+
Field: input.Field,
101+
ContentType: input.ContentType,
102+
Extension: input.Extension,
103+
Fuzzy: input.Fuzzy,
104+
SortBy: input.SortBy,
105+
SortDesc: input.SortDesc,
106+
MinSize: input.MinSize,
107+
MaxSize: input.MaxSize,
108+
ModifiedAfter: input.ModifiedAfter,
109+
Facets: input.Facets,
110+
Folder: input.Folder,
111+
ExifMake: input.ExifMake,
112+
ExifModel: input.ExifModel,
113+
ExifDateAfter: input.ExifDateAfter,
114+
ExifDateBefore: input.ExifDateBefore,
115+
ExifMinISO: input.ExifMinISO,
116+
ExifMaxISO: input.ExifMaxISO,
117+
ExifMinAperture: input.ExifMinAperture,
118+
ExifMaxAperture: input.ExifMaxAperture,
119+
ExifMinFocalLen: input.ExifMinFocalLen,
120+
ExifMaxFocalLen: input.ExifMaxFocalLen,
121+
ExifLatMin: input.ExifLatMin,
122+
ExifLatMax: input.ExifLatMax,
123+
ExifLonMin: input.ExifLonMin,
124+
ExifLonMax: input.ExifLonMax,
95125
}
96126

97127
result, err := srv.Indexer.SearchWithOptions(opts)

0 commit comments

Comments
 (0)