2024年7月

前言

有人说现在记事类app这么多,市场这么卷,为什么还想做一个笔记类App?

一来,去年小孩刚出生,需要一个可以记录喂奶时间的app,发现市面上没有一款app能够在两步内简单记录一个时间,可能iOS可以通过备忘录配合捷径做到快速记录,但是安卓上就没有类似的app。

二是,自去年做的音乐播放器以来,很长一段时间我在博客上的XF,MAUI都是在介绍局部的功能,[MAUI 项目实战]专题也很长没更新了,这次通过笔记类App做一次完整项目,包括如何上架MAUI应用等内容一并更新了。

一开始用MAUI简单做了一个功能,就记录喂奶量 + 喂奶时间 + 提醒。后期逐步做成一个可以动态添加摘要片段的功能。取名为《凡事摘要》。

目前安卓版本已发布打包,并上架腾讯应用商城:
凡事摘要
,iOS版本的部分页面还有问题需要调试,最近比较忙,我会抽出时间继续完善。在这个时间点我决定把App所有的代码都放在GitHub上,方便大家学习。也欢迎有兴趣的小伙伴一起参与。

框架

使用Abp框架,我之前写过如何
将Abp移植进.NET MAUI项目
,本项目也是按照这篇博文完成项目搭建。

这次的项目,主要通过原型和工厂模式建设基于模板的笔记内容。

没有使用过多的跨平台特性,如果需要了解更多MAUI跨平台知识,请参考之前
音乐播放器系列文章

定义

  • Note - 笔记,可以成整页打开的内容
  • NoteTemplate - 笔记模板,或称为场景,是可以快速创建笔记的模板
  • NoteSegment - 笔记片段,它是一个笔记(Note)的组成
  • NoteSegmentTemplate - 笔记片段模板,对应场景中可快速创建笔记片段的模板
  • NoteSegmentPayload - 笔记片段负载,存储具体笔记片段的内容
  • NoteSegmentService - 笔记片段服务类,为笔记片段,或笔记片段模板提供增删改等具体的业务逻辑
  • NoteSegmentServiceFactory - 笔记片段服务工厂,为笔记片段服务类提供工厂方法

核心类

INoteSegment:它是笔记片段的抽象类,模板类NoteSegmentTemplate和笔记片段类NoteSegment都实现了INoteSegment

在这里插入图片描述

它包含了笔记片段的属性,如标题、颜色、图标、是否隐藏、是否可删除、排序、状态、类型等。同时它关联一个笔记片段负载类INoteSegmentPayload

public interface INoteSegment
{
    string Color { get; set; }
    string Desc { get; set; }
    string Icon { get; set; }
    bool IsHidden { get; set; }
    bool IsRemovable { get; set; }
    int Rank { get; set; }
    string Status { get; set; }
    string Title { get; set; }
    string Type { get; set; }

    INoteSegmentPayload GetNoteSegmentPayload(string key);
    INoteSegmentPayload GetOrSetNoteSegmentPayload(string key, INoteSegmentPayload noteSegmentPayload);
    void SetNoteSegmentPayload(INoteSegmentPayload noteSegmentPayload);
}

INoteSegementService:凡事摘要拥有不同的笔记类型,如:时间戳片段,文本片段,文件片段等,App中可以通过添加片段按钮查看所有类型。

在这里插入图片描述

这些片段通过片段服务类(NoteSegementService)来描述该如何存储,使用Payload中的数据。

不同的片段类型,通过不同的片段服务类来实现。比如,在时间戳片段中,我们要存储当前时间和计算倒计时,而文件片段中,我们要存储文件路径,文件名,文件大小,文件类型等信息。

这些都是通过片段服务类来实现的。

在这里插入图片描述

具体类型如下:

类型 描述
DateTimeSegmentService 时间戳片段服务类
KeyValueSegmentService 数值片段服务类
FileSegmentService 文件片段服务类
TextSegmentService 文本片段服务类
TodoSegmentService 待办片段服务类
WeatherSegmentService 天气片段服务类
LocationSegmentService 地点片段服务类
TimerSegmentService 闹钟片段服务类
ContactSegmentService 联系人片段服务类
VoiceSegmentService 录音片段服务类
MediaSegmentService 媒体片段服务类
ScriptSegmentService 绘制片段服务类
DocumentSegmentService 文件片段服务类

片段服务类包含了一个INoteSegment,它是当前的笔记片段对象

在这里插入图片描述

INoteSegmentServiceFactory:

片段服务类的工厂类,除此之外还有一个INoteSegmentTemplateServiceFactory,他们都是根据笔记片段,或者笔记模板中的片段类型创建对应的片段服务类。

在这里插入图片描述

用于笔记的片段服务类的工厂类:

public interface INoteSegmentServiceFactory: ISingletonDependency
{
    INoteSegmentService GetNoteSegmentService(NoteSegment noteSegment);
}

用于笔记模板的片段服务类的工厂类

public interface INoteSegmentTemplateServiceFactory: ISingletonDependency
{
    INoteSegmentService GetNoteSegmentService(NoteSegmentTemplate noteSegmentTemplate);
}

NoteSegmentService作为笔记片段服务的基类,它继承了
ViewModelBase
,实际上服务基类是各笔记片段视图层的ViewModel,视图界面元素通过绑定服务类中的属性来显示或更新数据。

在这里插入图片描述

每一种服务类都对应的一个视图。渲染时,Xaml通过NoteSegmentDataTemplateSelector模板选择器来选择对应的视图。有关界面部分将在另一篇文章介绍。

项目地址

GitHub:MatoProductivity

在吕毅大佬的文章中已经详细介绍了什么是AppBar:
WPF 使用 AppBar 将窗口停靠在桌面上,让其他程序不占用此窗口的空间(附我封装的附加属性) - walterlv

即让窗口固定在屏幕某一边,并且保证其他窗口最大化后不会覆盖AppBar占据区域(类似于Windows任务栏)。

但是在我的环境中测试时,上面的代码出现了一些问题,例如非100%缩放显示时的坐标计算异常、多窗口同时停靠时布局错乱等。所以我重写了AppBar在WPF上的实现,效果如图:


一、AppBar的主要申请流程

主要流程如图:

核心代码其实在于如何计算停靠窗口的位置,要点是处理好一下几个方面:

1. 修改停靠位置时用原窗口的大小计算,被动告知需要调整位置时用即时大小计算

2. 像素单位与WPF单位之间的转换

3. 小心Windows的位置建议,并排停靠时会得到负值高宽,需要手动适配对齐方式

4. 有新的AppBar加入时,窗口会被系统强制移动到工作区(WorkArea),这点我还没能找到解决方案,只能把移动窗口的命令通过Dispatcher延迟操作

二、如何使用

1.下载我封装好的库:
AppBarTest/AppBarCreator.cs at master · TwilightLemon/AppBarTest (github.com)

2.  在xaml中直接设置:

<Window...>

<local:AppBarCreator.AppBar>
    <local:AppBarx:Name="appBar"Location="Top"OnFullScreenStateChanged="AppBar_OnFullScreenStateChanged"/>
</local:AppBarCreator.AppBar>...</Window>

或者在后台创建:

private readonly AppBar appBar=newAppBar();

...Window_Loaded...
appBar.Location
=AppBarLocation.Top;
appBar.OnFullScreenStateChanged
+=AppBar_OnFullScreenStateChanged;
AppBarCreator.SetAppBar(
this, appBar);

3. 另外你可能注意到了,这里有一个OnFullScreenStateChanged事件:该事件由AppBarMsg注册,在有窗口进入或退出全屏时触发,参数bool为true指示进入全屏。

你需要手动在事件中设置全屏模式下的行为,例如在全屏时隐藏AppBar

    private void AppBar_OnFullScreenStateChanged(object sender, boole)
{
Debug.WriteLine(
"Full Screen State:"+e);
Visibility
= e ?Visibility.Collapsed : Visibility.Visible;
}

我在官方的Flag上加了一个RegisterOnly,即只注册AppBarMsg而不真的停靠窗口,可以此用来作全屏模式监听。

4. 如果你需要在每个虚拟桌面都显示AppBar(像任务栏那样),可以尝试为窗口使用SetWindowLong添加WS_EX_TOOLWINDOW标签(自行查找)

以下贴出完整的代码:

1 usingSystem.ComponentModel;2 usingSystem.Diagnostics;3 usingSystem.Runtime.InteropServices;4 usingSystem.Windows;5 usingSystem.Windows.Interop;6 usingSystem.Windows.Threading;7 
8 namespaceAppBarTest;9 public static classAppBarCreator10 {11     public static readonly DependencyProperty AppBarProperty =
12 DependencyProperty.RegisterAttached(13             "AppBar",14             typeof(AppBar),15             typeof(AppBarCreator),16             new PropertyMetadata(null, OnAppBarChanged));17     private static voidOnAppBarChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)18 {19         if (d is Window window && e.NewValue isAppBar appBar)20 {21             appBar.AttachedWindow =window;22 }23 }24     public static voidSetAppBar(Window element, AppBar value)25 {26         if (value == null) return;27 element.SetValue(AppBarProperty, value);28 }29 
30     public staticAppBar GetAppBar(Window element)31 {32         return(AppBar)element.GetValue(AppBarProperty);33 }34 }35 
36 public classAppBar : DependencyObject37 {38     /// <summary>
39     ///附加到的窗口40     /// </summary>
41     publicWindow AttachedWindow42 {43         get =>_window;44         set
45 {46             if (value == null) return;47             _window =value;48             _window.Closing +=_window_Closing;49             _window.LocationChanged +=_window_LocationChanged;50             //获取窗口句柄hWnd
51             var handle = newWindowInteropHelper(value).Handle;52             if (handle ==IntPtr.Zero)53 {54                 //Win32窗口未创建
55                 _window.SourceInitialized +=_window_SourceInitialized;56 }57             else
58 {59                 _hWnd =handle;60 CheckPending();61 }62 }63 }64 
65     private void _window_LocationChanged(object?sender, EventArgs e)66 {67         Debug.WriteLine(_window.Title+ "LocationChanged: Top:"+_window.Top+"Left:"+_window.Left);68 }69 
70     private void _window_Closing(object?sender, CancelEventArgs e)71 {72         _window.Closing -=_window_Closing;73         if (Location !=AppBarLocation.None)74 DisableAppBar();75 }76 
77     /// <summary>
78     ///检查是否需要应用之前的Location更改79     /// </summary>
80     private voidCheckPending()81 {82         //创建AppBar时提前触发的LocationChanged
83         if(_locationChangePending)84 {85             _locationChangePending = false;86 LoadAppBar(Location);87 }88 }89     /// <summary>
90     ///载入AppBar91     /// </summary>
92     /// <param name="e"></param>
93     private void LoadAppBar(AppBarLocation e,AppBarLocation? previous=null)94 {95         
96         if (e !=AppBarLocation.None)97 {98             if (e ==AppBarLocation.RegisterOnly)99 {100                 //仅注册AppBarMsg101                 //如果之前注册过有效的AppBar则先注销,以还原位置
102                 if (previous.HasValue && previous.Value !=AppBarLocation.RegisterOnly)103 {104                     if (previous.Value !=AppBarLocation.None)105 {106                         //由生效的AppBar转为RegisterOnly,还原为普通窗口再注册空AppBar
107 DisableAppBar();108 }109 RegisterAppBarMsg();110 }111                 else
112 {113                     //之前未注册过AppBar,直接注册
114 RegisterAppBarMsg();115 }116 }117             else
118 {119                 if (previous.HasValue && previous.Value !=AppBarLocation.None)120 {121                     //之前为RegisterOnly才备份窗口信息
122                     if(previous.Value ==AppBarLocation.RegisterOnly)123 {124 BackupWindowInfo();125 }126 SetAppBarPosition(_originalSize);127 ForceWindowStyles();128 }129                 else
130 EnableAppBar();131 }132 }133         else
134 {135 DisableAppBar();136 }137 }138     private void _window_SourceInitialized(object?sender, EventArgs e)139 {140         _window.SourceInitialized -=_window_SourceInitialized;141         _hWnd = newWindowInteropHelper(_window).Handle;142 CheckPending();143 }144 
145     /// <summary>
146     ///当有窗口进入或退出全屏时触发 bool参数为true时表示全屏状态147     /// </summary>
148     public event EventHandler<bool>?OnFullScreenStateChanged;149     /// <summary>
150     ///期望将AppBar停靠到的位置151     /// </summary>
152     publicAppBarLocation Location153 {154         get { return(AppBarLocation)GetValue(LocationProperty); }155         set{ SetValue(LocationProperty, value); }156 }157 
158     public static readonly DependencyProperty LocationProperty =
159 DependencyProperty.Register(160             "Location",161             typeof(AppBarLocation), typeof(AppBar),162             newPropertyMetadata(AppBarLocation.None, OnLocationChanged));163 
164     private bool _locationChangePending = false;165     private static voidOnLocationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)166 {167         if(DesignerProperties.GetIsInDesignMode(d))168             return;169         if (d is not AppBar appBar) return;170         if (appBar.AttachedWindow == null)171 {172             appBar._locationChangePending = true;173             return;174 }175 appBar.LoadAppBar((AppBarLocation)e.NewValue,(AppBarLocation)e.OldValue);176 }177 
178     private int _callbackId = 0;179     private bool _isRegistered = false;180     private Window _window = null;181     privateIntPtr _hWnd;182     privateWindowStyle _originalStyle;183     privatePoint _originalPosition;184     private Size _originalSize =Size.Empty;185     privateResizeMode _originalResizeMode;186     private bool_originalTopmost;187     public Rect? DockedSize { get; set; } = null;188     private IntPtr WndProc(IntPtr hwnd, intmsg, IntPtr wParam,189                                     IntPtr lParam, ref boolhandled)190 {191         if (msg ==_callbackId)192 {193             Debug.WriteLine(_window.Title + "AppBarMsg("+_callbackId+"):" + wParam.ToInt32() + "LParam:" +lParam.ToInt32());194             switch(wParam.ToInt32())195 {196                 case (int)Interop.AppBarNotify.ABN_POSCHANGED:197                     Debug.WriteLine("AppBarNotify.ABN_POSCHANGED !"+_window.Title);198                     if (Location !=AppBarLocation.RegisterOnly)199 SetAppBarPosition(Size.Empty);200                     handled = true;201                     break;202                 case (int)Interop.AppBarNotify.ABN_FULLSCREENAPP:203                     OnFullScreenStateChanged?.Invoke(this, lParam.ToInt32() == 1);204                     handled = true;205                     break;206 }207 }208         returnIntPtr.Zero;209 }210 
211     public voidBackupWindowInfo()212 {213         _callbackId = 0;214         DockedSize = null;215         _originalStyle =_window.WindowStyle;216         _originalSize = newSize(_window.ActualWidth, _window.ActualHeight);217         _originalPosition = newPoint(_window.Left, _window.Top);218         _originalResizeMode =_window.ResizeMode;219         _originalTopmost =_window.Topmost;220 }221     public voidRestoreWindowInfo()222 {223         if (_originalSize !=Size.Empty)224 {225             _window.WindowStyle =_originalStyle;226             _window.ResizeMode =_originalResizeMode;227             _window.Topmost =_originalTopmost;228             _window.Left =_originalPosition.X;229             _window.Top =_originalPosition.Y;230             _window.Width =_originalSize.Width;231             _window.Height =_originalSize.Height;232 }233 }234     public voidForceWindowStyles()235 {236         _window.WindowStyle =WindowStyle.None;237         _window.ResizeMode =ResizeMode.NoResize;238         _window.Topmost = true;239 }240 
241     public voidRegisterAppBarMsg()242 {243         var data = newInterop.APPBARDATA();244         data.cbSize =Marshal.SizeOf(data);245         data.hWnd =_hWnd;246 
247         _isRegistered = true;248         _callbackId =Interop.RegisterWindowMessage(Guid.NewGuid().ToString());249         data.uCallbackMessage =_callbackId;250         var success = Interop.SHAppBarMessage((int)Interop.AppBarMsg.ABM_NEW, refdata);251         var source =HwndSource.FromHwnd(_hWnd);252         Debug.WriteLineIf(source == null, "HwndSource is null!");253         source?.AddHook(WndProc);254         Debug.WriteLine(_window.Title+"RegisterAppBarMsg:" +_callbackId);255 }256     public voidEnableAppBar()257 {258         if (!_isRegistered)259 {260             //备份窗口信息并设置窗口样式
261 BackupWindowInfo();262             //注册成为AppBar窗口
263 RegisterAppBarMsg();264 ForceWindowStyles();265 }266         //成为AppBar窗口之后(或已经是)只需要注册并移动窗口位置即可
267 SetAppBarPosition(_originalSize);268 }269     public voidSetAppBarPosition(Size WindowSize)270 {271         var data = newInterop.APPBARDATA();272         data.cbSize =Marshal.SizeOf(data);273         data.hWnd =_hWnd;274         data.uEdge = (int)Location;275         data.uCallbackMessage =_callbackId;276         Debug.WriteLine("\r\nWindow:"+_window.Title);277 
278         //获取WPF单位与像素的转换矩阵
279         var compositionTarget = PresentationSource.FromVisual(_window)?.CompositionTarget;280         if (compositionTarget == null)281             throw new Exception("居然获取不到CompositionTarget?!");282         var toPixel =compositionTarget.TransformToDevice;283         var toWpfUnit =compositionTarget.TransformFromDevice;284 
285         //窗口在屏幕的实际大小
286         if(WindowSize==Size.Empty)287             WindowSize = newSize(_window.ActualWidth, _window.ActualHeight);288         var actualSize = toPixel.Transform(newVector(WindowSize.Width, WindowSize.Height));289         //屏幕的真实像素
290         var workArea = toPixel.Transform(newVector(SystemParameters.PrimaryScreenWidth, SystemParameters.PrimaryScreenHeight));291         Debug.WriteLine("WorkArea Width: {0}, Height: {1}", workArea.X, workArea.Y);292 
293         if (Location isAppBarLocation.Left or AppBarLocation.Right)294 {295             data.rc.top = 0;296             data.rc.bottom = (int)workArea.Y;297             if (Location ==AppBarLocation.Left)298 {299                 data.rc.left = 0;300                 data.rc.right =  (int)Math.Round(actualSize.X);301 }302             else
303 {304                 data.rc.right = (int)workArea.X;305                 data.rc.left = (int)workArea.X - (int)Math.Round(actualSize.X);306 }307 }308         else
309 {310             data.rc.left = 0;311             data.rc.right = (int)workArea.X;312             if (Location ==AppBarLocation.Top)313 {314                 data.rc.top = 0;315                 data.rc.bottom = (int)Math.Round(actualSize.Y);316 }317             else
318 {319                 data.rc.bottom = (int)workArea.Y;320                 data.rc.top = (int)workArea.Y - (int)Math.Round(actualSize.Y);321 }322 }323         //以上生成的是四周都没有其他AppBar时的理想位置324         //系统将自动调整位置以适应其他AppBar
325         Debug.WriteLine("Before QueryPos: Left: {0}, Top: {1}, Right: {2}, Bottom: {3}", data.rc.left, data.rc.top, data.rc.right, data.rc.bottom);326         Interop.SHAppBarMessage((int)Interop.AppBarMsg.ABM_QUERYPOS, refdata);327         Debug.WriteLine("After QueryPos: Left: {0}, Top: {1}, Right: {2}, Bottom: {3}", data.rc.left, data.rc.top, data.rc.right, data.rc.bottom);328         //自定义对齐方式,确保Height和Width不会小于0
329         if (data.rc.bottom - data.rc.top < 0)330 {331             if (Location ==AppBarLocation.Top)332                 data.rc.bottom = data.rc.top + (int)Math.Round(actualSize.Y);//上对齐
333             else if (Location ==AppBarLocation.Bottom)334                 data.rc.top = data.rc.bottom - (int)Math.Round(actualSize.Y);//下对齐
335 }336         if(data.rc.right - data.rc.left < 0)337 {338             if (Location ==AppBarLocation.Left)339                 data.rc.right = data.rc.left + (int)Math.Round(actualSize.X);//左对齐
340             else if (Location ==AppBarLocation.Right)341                 data.rc.left = data.rc.right - (int)Math.Round(actualSize.X);//右对齐
342 }343         //调整完毕,设置为最终位置
344         Interop.SHAppBarMessage((int)Interop.AppBarMsg.ABM_SETPOS, refdata);345         //应用到窗口
346         var location = toWpfUnit.Transform(newPoint(data.rc.left, data.rc.top));347         var dimension = toWpfUnit.Transform(new Vector(data.rc.right -data.rc.left,348                                                                                     data.rc.bottom -data.rc.top));349         var rect = new Rect(location, newSize(dimension.X, dimension.Y));350         DockedSize =rect;351 
352         _window.Dispatcher.Invoke(DispatcherPriority.ApplicationIdle, () =>{353         _window.Left =rect.Left;354         _window.Top =rect.Top;355         _window.Width =rect.Width;356         _window.Height =rect.Height;357 });358 
359         Debug.WriteLine("Set {0} Left: {1} ,Top: {2}, Width: {3}, Height: {4}", _window.Title, _window.Left, _window.Top, _window.Width, _window.Height);360 }361     public voidDisableAppBar()362 {363         if(_isRegistered)364 {365             _isRegistered = false;366             var data = newInterop.APPBARDATA();367             data.cbSize =Marshal.SizeOf(data);368             data.hWnd =_hWnd;369             data.uCallbackMessage =_callbackId;370             Interop.SHAppBarMessage((int)Interop.AppBarMsg.ABM_REMOVE, refdata);371             _isRegistered = false;372 RestoreWindowInfo();373             Debug.WriteLine(_window.Title + "DisableAppBar");374 }375 }376 }377 
378 public enum AppBarLocation : int
379 {380     Left = 0,381 Top,382 Right,383 Bottom,384 None,385     RegisterOnly=99
386 }387 
388 internal static classInterop389 {390     #region Structures & Flags
391 [StructLayout(LayoutKind.Sequential)]392     internal structRECT393 {394         public intleft;395         public inttop;396         public intright;397         public intbottom;398 }399 
400 [StructLayout(LayoutKind.Sequential)]401     internal structAPPBARDATA402 {403         public intcbSize;404         publicIntPtr hWnd;405         public intuCallbackMessage;406         public intuEdge;407         publicRECT rc;408         publicIntPtr lParam;409 }410 
411     internal enum AppBarMsg : int
412 {413         ABM_NEW = 0,414 ABM_REMOVE,415 ABM_QUERYPOS,416 ABM_SETPOS,417 ABM_GETSTATE,418 ABM_GETTASKBARPOS,419 ABM_ACTIVATE,420 ABM_GETAUTOHIDEBAR,421 ABM_SETAUTOHIDEBAR,422 ABM_WINDOWPOSCHANGED,423 ABM_SETSTATE424 }425     internal enum AppBarNotify : int
426 {427         ABN_STATECHANGE = 0,428 ABN_POSCHANGED,429 ABN_FULLSCREENAPP,430 ABN_WINDOWARRANGE431 }432     #endregion
433 
434     #region Win32 API
435     [DllImport("SHELL32", CallingConvention =CallingConvention.StdCall)]436     internal static extern uint SHAppBarMessage(int dwMessage, refAPPBARDATA pData);437 
438     [DllImport("User32.dll", CharSet =CharSet.Auto)]439     internal static extern int RegisterWindowMessage(stringmsg);440     #endregion
441 }

三、已知问题

1.在我的github上的实例程序中,如果你将两个同进程的窗口并排叠放的话,会导致explorer和你的进程双双爆栈,windows似乎不能很好地处理这两个并排放置地窗口,一直在左右调整位置,疯狂发送ABN_POSCHANGED消息。(快去clone试试,死机了不要打我) 但是并排放置示例窗口和OneNote地Dock窗口就没有问题。

2.计算停靠窗口时,如果选择停靠位置为Bottom,则系统建议的bottom位置值会比实际的高,测试发现是任务栏窗口占据了部分空间,应该是预留给平板模式的更大图标任务栏(猜测,很不合理的设计)

自动隐藏任务栏就没有这个问题:

3. 没有实现自动隐藏AppBar,故没有处理与之相关的WM_ACTIVATE等消息,有需要的可以参考官方文档。(嘻 我懒)

参考文档:

1).
SHAppBarMessage function (shellapi.h) - Win32 apps | Microsoft Learn

2).
ABM_QUERYPOS message (Shellapi.h) - Win32 apps | Microsoft Learn
ABM_NEW & ABM_SETPOS etc..

3).
使用应用程序桌面工具栏 - Win32 apps | Microsoft Learn

4).
判断是否有全屏程序正在运行(C#)_c# 判断程序当前窗口是否全屏如果是返回原来-CSDN博客

[打个广告] [入门AppBar的最佳实践]

看这里,如果你也需要一个高度可自定义的沉浸式顶部栏(Preview):
TwilightLemon/MyToolBar: 为Surface Pro而生的顶部工具栏 支持触控和笔快捷方式 (github.com)


本作品采用
知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议
进行许可。欢迎转载、使用、重新发布,但务必保留文章署名TwilightLemon和原文网址,不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

前言:
学习ComfyUI是一场持久战,而Comfyroll 是一款功能强大的自定义节点集合,专为 ComfyUI 用户打造,旨在提供更加丰富和专业的图像生成与编辑工具。借助这些节点,用户可以在静态图像的精细调整和动态动画的复杂构建方面进行深入探索。Comfyroll 的节点设计简洁易用,功能强大,是每个希望提升 ComfyUI 使用体验的用户的必备选择。祝大家学习顺利,早日成为ComfyUI的高手!

目录
一、安装方法
二、CR Text List节点
三、CR Prompt List节点
四、CR Simple List节点
五、CR Float Range List节点
六、CR Integer Range List节点
七、CR Binary To Bit List节点
八、CR Value Cycler/CR Text Cycler 节点
九、“人物一致性控制”示例工作流

一、安装方法
在ComfyUI主目录里面输入CMD回车。

在弹出的CMD命令行输入git clone
https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes.git,即可开始下载。

二、CR Text List节点
该节点用于创建和管理一个包含文本元素的列表,主要功能是接受多个文本输入并将它们组织成一个列表,以便在工作流中进行进一步处理。

输入:
文本框 → 输入多行文本
不需要符号隔开,用回车隔开即好
参数:
start_index → 表示开始位置的索引
从0开始计数
max_rows → 表示选取最大行数
输出:
STRING → 输出的文本信息
多行单次输出
Eg:如下图所示我们可以利用该特性建造如下"人物一致性工作流",通过控制种子不变,提示词输入大体提示词,然后通过CR Text List节点去控制我们对提示词进行微调,比如仅控制动作,姿势,脸部表情等信息,去生成不同但是相似的人物。

使用场景
· 批量文本处理: 当你有多个文本段落或句子需要处理时,可以使用这个节点将它们组织成一个列表,以便批量处理。
· 数据管理: 在数据管理任务中,将多个文本数据整合到一个列表中,便于后续的分析或操作。
· 自动化工作流: 在自动化文本生成或处理工作流中,用于批量管理和传递文本数据。
通过 CR Text List 节点,可以轻松地创建和管理文本列表,从而在 ComfyUI 中实现高效的文本数据处理和管理。

三、CR Prompt List节点
该节点用于创建和管理提示词列表,通常用于生成图像或其他需要批量提示词的任务,主要功能是接受多个提示词输入并将它们组织成一个列表,以便在工作流中进行进一步处理,例如用于图像生成、文本生成等任务。

输入:
prepend_text → 加在每一行文本前的文字
文本框 → 输入多行文本,用回车隔开
参数:
append_text → 加在每一行文本后的文字
start_index → 开始的行数索引,从0开始
max_rows → 最长选取的行数
输出:
prompt → 带有前缀和后缀的列表行
body_text → 原始列表行
如下图所示我们可以通过该提示词输入框进行批量文本输入,产生不同类型的图像。

使用场景
· 图像生成: 当你需要批量生成图像时,可以使用这个节点将多个提示词组织成一个列表,以便生成对应的图像。
· 文本生成: 在需要批量生成文本内容时,将多个提示词组织成一个列表,以便生成对应的文本。
· 自动化工作流: 在自动化内容生成工作流中,用于批量管理和传递提示词数据。
通过 CR Prompt List 节点,可以轻松地创建和管理提示词列表,从而在 ComfyUI 中实现高效的批量内容生成和管理。

四、CR Simple List节点
该节点用于创建和管理一个简单的数据列表,可以包含任意类型的元素,主要功能是接受多个输入元素并将它们组织成一个列表,以便在工作流中进行进一步处理。这些元素可以是任意类型的数据,如文本、数值、图像路径等。

输入:
文本输入框 → 输入多行文本,用回车隔开
输出:
LIST → 输出文本列表
注意:如下图所示我们文本输出是一个列表的形式,即按行进行单行的输出。

使用场景
· 数据整合: 将多种类型的数据整合到一个列表中,便于后续统一处理。
· 批量处理: 在需要批量处理不同类型的数据时,使用该节点创建一个包含所有数据的列表。
· 自动化工作流: 在自动化工作流中,用于批量管理和传递多种类型的数据。
通过 CR Simple List 节点,可以轻松地创建和管理包含多种类型数据的列表,从而在 ComfyUI 中实现高效的数据整合和批量处理。

五、CR Float Range List节点
该节点用于生成一个指定范围内的浮点数列表,主要功能是根据用户定义的起始值、结束值和步长,生成一个浮点数的列表。这对于需要处理一系列连续或离散浮点数的任务非常有用。

参数:
start → 范围的起始值
end → 范围的结束值
step → 步长,用于确定相邻两个值之间的间隔
operation → 要应用于列表值的数学运算,可以是 "none"、"sin"、"cos" 或 "tan"
decimal_places → 结果中的小数位数
ignore_first_value → 是否忽略生成范围的第一个值
max_values_per_loop → 每次循环生成的最大值数
loops → 要执行的循环次数
ping_pong → 是否应用“乒乓”模式,即在偶数次循环中反向生成范围
输出:
FLOAT → 生成的浮点数范围列表
如下图所示我们设置起点为0.00,终点为10.00,按照步长为1.00进行循环,不忽略起始值,并且设置循环2次,使用ping_pong模式,最终生成的图像从数值0到10为第一行,因为开启ping_pong所以在第二行为10到0.

使用场景
· 数值分析: 生成一个浮点数范围,用于数值分析或模拟。
· 参数扫描: 在机器学习或优化任务中,生成一组参数值用于扫描和测试。
· 数据生成: 在数据生成任务中,创建一系列连续或离散的浮点数数据。
通过 CR Float Range List 节点,可以轻松生成一个指定范围和步长的浮点数列表,从而在 ComfyUI 中实现高效的数值处理和分析。

六、CR Integer Range List节点
该节点用于生成一个指定范围内的整数列表,主要功能是根据用户定义的起始值、结束值和步长,生成一个整数的列表。这对于需要处理一系列连续或离散整数的任务非常有用。

参数:
start → 范围的起始值
end → 范围的结束值
step → 步长,用于确定相邻两个值之间的间隔
loops → 要执行的循环次数
ping_pong → 是否应用“乒乓”模式,即在偶数次循环中反向生成范围
输出:
INT → 生成的整数范围列表
Eg1:如下图所示,我们设置其实值为0到200,步长为5,循环一次,输出控制图像的x坐标值,从而使得我们最终生成的五角星图案从左边移动到右边。

Eg2:如下图所示,基于此节点我们可以改进"人物一致性控制工作流",不仅在种子上设置固定去控制我们的人物相似,在传入噪声图时,我们使用该节点去生成类似并且变化幅度很小的图像去做为地图,供Ksampler去扩散,最终生成我们人物几乎相似,但是表情不同,方位不同的图像。

使用场景
· 数值分析: 生成一个整数范围,用于数值分析或模拟。
· 参数扫描: 在机器学习或优化任务中,生成一组参数值用于扫描和测试。
· 数据生成: 在数据生成任务中,创建一系列连续或离散的整数数据。
通过 CR Integer Range List 节点,可以轻松生成一个指定范围和步长的整数列表,从而在 ComfyUI 中实现高效的数值处理和分析。

七、CR Binary To Bit List节点
该节点主要功能是用于将一个二进制数转换为一个比特(bi列表,这个列表由 0 和 1 组成,表示输入二进制数的每一位。

输入:
文本框输入 → 包含二进制字符串的字符串
输出:
STRING → 生成的位列表,其中每个元素表示输入字符串中的一个字符。
如下图所示,我们在输入框输入0000 0011 0000 0000,生成的图像为4*4的网格,每个网格为单独的一张图像,在数字为"1"的地方,我们生成的颜色与数字为"0"的地方生成的图像颜色不一样。

使用场景
· 数字信号处理: 在处理二进制数据或信号时,将二进制数转换为比特列表以便逐位处理。
· 二进制操作: 在需要对二进制数进行比特级别操作时,将其转换为比特列表进行处理。
· 数据分析: 将二进制数据转换为比特列表,以便进行数据分析或模式识别。
通过 CR Binary To Bit List 节点,可以将二进制数转换为比特列表,从而在 ComfyUI 中实现比特级别的数据处理和分析。

八、CR Value Cycler/CR Text Cycler 节点
CR Value Cycler 节点用于循环处理一组数值。每次运行时,该节点会按顺序输出列表中的下一个数值,当到达列表末尾时,将从头开始。CR Text Cycler 节点用于循环处理文本元素,每次运行时,该节点会按顺序输出列表中的下一个文本,当到达列表末尾时,将从头开始。

输入:
文本输入框 → 包含要循环的值,可以是多行
参数:
repeats → 每个值在结果中重复出现的次数,默认为 1。最小值为 1,最大值为 99999。
loops → 循环生成值的次数,默认为 1。最小值为 1,最大值为 99999。
输出:
FLOAT → 以浮点数输出值
INT → 以整数型输出值
STRING → 以字符串输出值

注意:repeats和loops实际产生的情况是一样的,就是重复多少遍,只是说repeats指示文字出现的次数,loops代表所有行的循环次数。
使用场景
①CR Value Cycler节点
· 参数循环: 在参数优化或测试中,依次循环使用一组参数值。
· 任务调度: 在自动化任务中,依次分配任务参数。
· 数据采样: 在数据处理流程中,依次输出数据样本进行处理。
②CR Text Cycler节点
· 提示词循环: 在生成任务中,依次使用一组提示词。
· 文本轮询: 在文本处理流程中,依次输出文本片段进行处理。
· 内容生成: 在内容生成工作流中,依次使用不同的文本模板或内容片段。
通过 CR Value Cycler 和 CR Text Cycler 节点,可以在 ComfyUI 中实现数值和文本的循环处理,从而在参数优化、任务调度和内容生成等场景中发挥重要作用。

九、“人物一致性控制”示例工作流
熟练使用以上节点,你就可以搭建“人物一致性控制”的工作流了。

这里使用SDXL的大模型,并且加载对应的LCM-lora用来控制图像的生成速度。ComfyUI的工作流,其主要核心还是思路,每个人的思路都是不一样的,希望大家在学习ComfyUI的时候不要被固定的套路困住阵脚,多发散。最后如果你有好的工作流搭建方向可以在评论区留言哦。

                                          **孜孜以求,方能超越自我。坚持不懈,乃是成功关键。**

今天,给大家推荐几个好用且免费的IntelliJ IDEA插件。如果你还没有用过,可以尝试一下,也许对你的日常工作会有一定的效率提升噢!

RestFul-Tool

如果你是一个RESTful服务的开发者,那么这个一定要试一下。它是一套非常丰富的RESTful服务开发工具,对 Spring MVC 和 Spring Boot 支持也是非常友好,开发者可以直接通过插件找到自己编写的RESTful接口:

同时也能非常方便的进行测试:

Spring Boot Assistant

如果你跟我一样,是一个Spring Boot开发人员,那么这个插件也是非常推荐的。有了这个插件之后,可以在我们编写程序的时候提出更多针对Spring Boot的有用提示。

Test Data

Test Data插件可以提高编写单元测试的效率,因为它可以为我们生成多种类型的测试数据,包括文本、UUID、数字、日期和时间,以及自定义类型,如JSON、CSV和SQL等流行的数据类型。

SonarLint

如果你喜欢SonarQube,那么SonarLint一定要看一下。SonarLint 是一款面向开发人员的开源工具,旨在确保代码符合开发和生产环境的标准。它有助于在您编写时实时纠正代码异味,不仅提供建议,还提供对问题及其相关风险的详细见解。

PlantUML

上面都跟代码相关,最后推荐一个与图相关的插件。PlantUML插件可以帮助我们好利用简单直观的语言,毫不费力地绘制各种类型的图表。

你还知道什么好用的插件吗?欢迎留言区分享!

欢迎关注我的公众号:程序猿DD。第一时间了解前沿行业消息、分享深度技术干货、获取优质学习资源

本文深入探讨了向量数据库的基础概念、架构设计及实现技术,详细介绍了HNSW、FAISS和Milvus等关键算法和工具,旨在为高效管理和检索高维向量数据提供全面的技术指南。

关注TechLead,复旦博士,分享云服务领域全维度开发技术。拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,复旦机器人智能实验室成员,国家级大学生赛事评审专家,发表多篇SCI核心期刊学术论文,阿里云认证的资深架构师,上亿营收AI产品研发负责人。

file

1. 引言

1.1 什么是向量数据库

file

向量数据库是一种专门用于存储和查询高维向量数据的数据库系统。在现代数据处理和人工智能应用中,越来越多的数据以高维向量的形式存在,例如图像特征、文本嵌入和用户行为等。传统的关系型数据库在处理这种高维数据时效率低下,而向量数据库则通过特定的索引结构和优化算法,使得高维向量的存储、管理和检索变得更加高效。

向量数据库不仅支持大规模向量数据的存储,还提供高效的相似性搜索功能,即快速找到与查询向量最相似的若干个向量。这在推荐系统、图像识别、自然语言处理等领域具有广泛的应用。向量数据库的核心技术包括向量空间模型、距离度量、索引构建和优化检索算法等。

1.2 向量数据库的起源与发展

向量数据库的概念并不是凭空出现的,它的起源可以追溯到信息检索和机器学习领域中的向量空间模型(Vector Space Model, VSM)。向量空间模型是一种用于信息检索的数学模型,它将文档和查询都表示为向量,通过计算向量之间的相似性来进行检索。这一模型在20世纪60年代由Gerard Salton提出,为向量数据库的发展奠定了理论基础。

随着计算机技术的发展,尤其是存储和计算能力的提升,高维数据处理成为可能。20世纪90年代,随着大规模数据集和复杂算法的出现,研究者们开始探索如何高效地存储和查询这些高维数据。2000年代,随着机器学习和深度学习的兴起,向量数据的需求急剧增加。例如,图像识别中的卷积神经网络(CNN)和自然语言处理中的词嵌入(Word Embeddings)都产生了大量的高维向量数据,这些数据需要专门的存储和处理技术。

近年来,向量数据库的发展进入了快车道,得益于以下几个方面的推动:

  1. 硬件技术的进步
    :包括大规模存储设备、GPU和分布式计算技术的发展,使得处理海量高维数据成为可能。
  2. 算法的优化
    :新的索引结构和检索算法(如HNSW、ANNOY、FAISS等)显著提高了向量数据的检索效率。
  3. 开源社区的推动
    :许多优秀的开源项目,如Milvus、Elasticsearch的向量搜索插件等,加速了向量数据库的普及和应用。

总的来说,向量数据库的发展是一个多领域交叉、持续创新的过程。从最初的信息检索模型,到现代复杂的深度学习应用,向量数据库在数据科学、人工智能和大数据领域发挥着越来越重要的作用。通过优化向量数据的存储和检索,向量数据库为各类应用提供了高效的数据支持,推动了技术进步和商业应用的创新。

2. 向量数据库的基础概念

2.1 向量空间模型

file

向量空间模型(Vector Space Model, VSM)是向量数据库的基础概念之一。VSM是信息检索领域中广泛使用的模型,它将文档和查询都表示为向量,利用向量之间的相似性进行检索。向量空间模型的核心思想是将文本数据转化为多维空间中的点,通过点之间的距离或夹角来衡量相似性。

在VSM中,每个文档或查询向量的维度通常表示词汇表中的一个词,向量的每个分量表示该词在文档或查询中的重要性。常见的权重计算方法包括词频-逆文档频率(TF-IDF)和词嵌入(Word Embeddings)。向量之间的相似性通常通过余弦相似度、欧氏距离或曼哈顿距离等度量方法来计算。

词频-逆文档频率(TF-IDF)

TF-IDF是一种统计方法,用于评估一个词在文档集合中的重要性。词频(TF)表示一个词在文档中出现的频率,而逆文档频率(IDF)衡量词在整个文档集合中的普遍性。TF-IDF的计算公式如下:

file

其中,(N)是文档集合中的文档总数,(n_t)是包含词(t)的文档数量。

词嵌入(Word Embeddings)

词嵌入是一种将词映射到低维连续向量空间的技术,使得相似词在向量空间中距离较近。常见的词嵌入方法包括Word2Vec、GloVe和FastText。词嵌入的关键在于通过神经网络模型学习词的上下文关系,从而生成具有语义信息的向量表示。这些向量表示可以用于文本分类、聚类和检索等任务。

2.2 向量检索的基本原理

向量检索是向量数据库的核心功能之一,即根据查询向量找到最相似的向量集合。向量检索的基本原理包括相似性度量、索引结构和检索算法。

相似性度量

向量相似性的度量方法有多种,常见的包括:
file

索引结构

为了提高向量检索的效率,向量数据库通常会构建索引结构。常见的索引结构包括:

  • 倒排索引
    :用于稀疏向量,记录每个词在文档中的出现位置。
  • 树形结构
    :如KD树(k-dimensional tree)和R树(R-tree),适用于低维向量的检索。
  • 图结构
    :如HNSW(Hierarchical Navigable Small World),适用于高维向量的近似最近邻搜索。

检索算法

向量检索算法旨在快速找到与查询向量最相似的若干个向量。常见的检索算法包括:

  • 暴力搜索
    :直接计算查询向量与数据库中所有向量的相似性,适用于小规模数据集。
  • 局部敏感哈希(LSH)
    :通过哈希函数将相似的向量映射到相同的桶中,从而减少计算量。
  • 近似最近邻搜索(ANN)
    :如FAISS(Facebook AI Similarity Search)和ANNOY(Approximate Nearest Neighbors Oh Yeah),通过构建近似索引结构,提高检索效率。

2.3 常用距离度量方法

在向量数据库中,距离度量方法是检索过程中的重要组成部分。除了余弦相似度、欧氏距离和曼哈顿距离外,还有其他几种常用的距离度量方法:

file

每种距离度量方法都有其适用的场景和特点,选择合适的距离度量方法对于提高向量检索的准确性和效率至关重要。

3. 向量数据库的架构

向量数据库的架构是其高效存储、管理和检索高维向量数据的基础。了解向量数据库的架构有助于我们更好地理解其工作原理,并在实际应用中进行优化。本章将深入探讨向量数据库的核心架构,包括数据存储与索引机制、查询处理与优化,以及并行与分布式计算。

3.1 数据存储与索引机制

向量数据库的存储与索引机制是其性能和效率的关键组成部分。高效的数据存储和索引可以显著提高向量检索的速度和准确性。

3.1.1 数据存储

向量数据的存储方式直接影响数据库的读取和写入性能。常见的存储方式包括:

  • 行存储(Row Storage)
    :将每个向量作为一行存储,每个分量作为一列。这种方式适用于频繁的逐行读取和写入操作。
  • 列存储(Column Storage)
    :将每个分量作为一列存储,每个向量的所有分量分散在不同的列中。这种方式适用于需要对特定维度进行聚合或筛选的操作。
  • 压缩存储(Compressed Storage)
    :通过对向量数据进行压缩存储,可以减少存储空间和I/O开销。常见的压缩技术包括稀疏向量的稀疏矩阵存储和密集向量的量化存储。

3.1.2 索引机制

索引机制是向量数据库中提升查询效率的重要手段。常见的索引结构包括:

  • 倒排索引(Inverted Index)
    :倒排索引将每个词或特征映射到包含该特征的向量ID列表。倒排索引适用于稀疏向量的相似性检索。

  • 树形索引(Tree-based Index)
    :包括KD树(k-dimensional tree)和R树(R-tree)等,适用于低维向量的精确最近邻搜索。KD树通过递归划分向量空间来构建索引,而R树则通过分层的最小包围矩形来组织向量数据。

  • 图索引(Graph-based Index)
    :如HNSW(Hierarchical Navigable Small World)和NSW(Navigable Small World)图,适用于高维向量的近似最近邻搜索。图索引通过构建小世界网络来提高检索效率,节点之间的连接表示向量之间的相似性。

3.2 查询处理与优化

查询处理与优化是向量数据库提供高效检索服务的核心。向量数据库需要处理大量的高维向量数据,优化查询处理过程对于提升系统性能至关重要。

3.2.1 查询处理流程

向量查询处理流程通常包括以下几个步骤:

  1. 查询解析
    :将用户输入的查询向量进行解析和预处理,包括向量归一化、特征选择等。
  2. 索引检索
    :根据预先构建的索引结构,快速筛选出与查询向量最相似的候选向量集合。
  3. 相似性计算
    :对候选向量集合进行相似性度量,计算查询向量与每个候选向量之间的距离或相似度。
  4. 结果排序
    :根据相似性度量结果,对候选向量进行排序,选择相似度最高的若干个向量作为最终结果。
  5. 结果返回
    :将排序后的相似向量结果返回给用户。

3.2.2 查询优化技术

为了提高查询处理效率,向量数据库通常采用多种优化技术,包括:

  • 并行查询
    :通过并行化查询处理,可以充分利用多核CPU和分布式计算资源,加速相似性检索过程。
  • 缓存机制
    :在内存中缓存常用的查询向量和索引结果,减少重复计算和I/O开销。
  • 近似算法
    :采用近似最近邻搜索(ANN)算法,如LSH(Locality-Sensitive Hashing)和PQ(Product Quantization),在保证结果精度的前提下,加快检索速度。
  • 剪枝策略
    :在索引检索和相似性计算过程中,采用剪枝策略剔除不可能的候选向量,减少计算量。

3.3 并行与分布式计算

随着数据规模的不断扩大和应用场景的复杂化,向量数据库需要支持并行和分布式计算,以提升处理能力和系统性能。

3.3.1 并行计算

并行计算通过将计算任务分解为多个子任务,并在多个处理器上同时执行,从而提高计算效率。向量数据库中的并行计算主要体现在以下几个方面:

  • 并行索引构建
    :在构建索引时,可以将向量数据划分为多个子集,分别在多个处理器上并行构建索引。
  • 并行查询处理
    :在进行相似性检索时,可以将查询向量分发到多个处理器,并行计算相似性度量结果。
  • 并行数据处理
    :在数据预处理和特征提取过程中,可以并行处理不同的数据块,提高处理速度。

3.3.2 分布式计算

分布式计算通过将计算任务分布到多个独立的计算节点上进行处理,从而扩展系统的处理能力和存储容量。向量数据库中的分布式计算主要体现在以下几个方面:

  • 分布式存储
    :将向量数据分布存储在多个节点上,提高数据的存储容量和访问速度。常见的分布式存储系统包括HDFS(Hadoop Distributed File System)和Cassandra等。
  • 分布式索引
    :在多个节点上并行构建和维护索引结构,提高索引构建和更新效率。
  • 分布式查询
    :将查询任务分发到多个计算节点,并行处理查询请求,汇总各节点的查询结果,提供高效的相似性检索服务。

4. 向量数据库的实现技术

向量数据库的实现技术涉及多种算法和工具,通过优化数据存储、索引构建和查询处理,实现高效的高维向量数据管理和检索。本章将深入探讨几种核心的实现技术,包括HNSW算法、FAISS、Milvus的架构与实现,帮助读者全面理解向量数据库的技术细节。

4.1 HNSW(Hierarchical Navigable Small World)算法

HNSW(Hierarchical Navigable Small World)是一种基于小世界图理论的近似最近邻搜索(ANN)算法。它通过构建一个分层的导航图结构,实现高效的高维向量相似性检索。

4.1.1 HNSW的基本原理

HNSW算法通过以下几个步骤构建和检索向量数据:

  1. 图构建
    :HNSW构建一个分层的图结构,每一层是一个稀疏的近似最近邻图。顶层包含较少的节点,每层向下节点数逐渐增加,底层包含所有的向量数据。
  2. 插入操作
    :向图中插入新向量时,从顶层开始,通过贪心搜索找到最接近的新节点的若干个邻居,然后逐层向下进行邻居更新,最终在底层插入新节点。
  3. 检索操作
    :从顶层的一个或多个入口节点开始,通过贪心搜索找到与查询向量最接近的节点,逐层向下进行精确搜索,最终在底层返回最相似的若干个向量。

4.1.2 HNSW的优势与应用

HNSW算法在检索效率和精度上具有显著优势,适用于大规模高维向量数据的近似最近邻搜索。其主要优势包括:

  • 高效性
    :通过分层结构和贪心搜索,大幅减少了检索过程中的计算量。
  • 高精度
    :能够在较低的计算开销下实现高精度的相似性检索。
  • 灵活性
    :支持动态插入和删除操作,适用于不断更新的数据集。

FAISS是由Facebook AI Research团队开发的开源库,用于高效的相似性搜索和密集向量聚类。它提供了多种索引和优化算法,能够处理数十亿规模的高维向量数据。

4.2.1 FAISS的核心功能

FAISS提供了多种索引结构和优化算法,主要包括:

  • 扁平索引(Flat Index)
    :适用于小规模数据集,通过暴力搜索实现精确最近邻检索。
  • 倒排文件索引(IVF, Inverted File Index)
    :将向量数据划分为若干个簇,通过簇中心进行初步筛选,提高检索效率。
  • PQ(Product Quantization)
    :将向量数据分块量化,减少存储空间和计算量。
  • HNSW索引
    :结合了HNSW算法,提供高效的近似最近邻搜索。

4.2.2 FAISS的应用场景

FAISS适用于以下应用场景:

  • 大规模图像搜索
    :利用卷积神经网络提取图像特征,进行高效的相似性搜索。
  • 自然语言处理
    :对文本数据进行词嵌入或句子嵌入,进行相似性检索和聚类。
  • 推荐系统
    :根据用户行为特征,进行相似用户或相似物品的快速检索。

4.3 Milvus的架构与实现

Milvus是一个开源的、高性能的向量数据库,旨在处理大规模高维向量数据。它采用多种先进的实现技术,以满足不同应用场景的需求。

4.3.1 Milvus的核心架构

Milvus的核心架构包括以下几个模块:

  • 存储引擎
    :支持多种存储后端,包括本地文件系统和分布式存储系统(如HDFS、S3等)。
  • 索引模块
    :提供多种索引结构,如IVF、PQ、HNSW等,根据数据特点和应用需求选择最优的索引策略。
  • 查询引擎
    :实现高效的相似性检索,支持并行查询和分布式计算。
  • 管理模块
    :提供数据导入、导出、备份和恢复等管理功能,保证数据的安全性和可用性。

4.3.2 Milvus的实现细节

Milvus的实现细节包括:

  • 多种索引策略
    :根据数据规模和查询需求,选择适当的索引策略进行优化。
  • 高效存储管理
    :采用内存映射文件和分层存储技术,提高数据的读写性能。
  • 并行和分布式计算
    :支持多线程并行处理和分布式计算,提升系统的处理能力和扩展性。
  • 自动调优
    :根据数据分布和查询负载,动态调整索引参数和查询策略,优化系统性能。

4.4 其他实现技术

除了上述几种主要技术,向量数据库还采用了其他一些实现技术,以提高系统性能和可靠性。

4.4.1 局部敏感哈希(LSH)

LSH(Locality-Sensitive Hashing)是一种近似最近邻搜索算法,通过将相似的向量映射到相同的哈希桶中,实现高效的相似性检索。LSH的主要优点是能够处理高维稀疏向量,但在处理密集向量时性能可能不如HNSW和FAISS。

4.4.2 产品量化(PQ)

PQ(Product Quantization)是一种向量量化技术,通过将向量数据分块,并对每个块进行独立的量化编码,实现数据压缩和加速检索。PQ在FAISS中得到了广泛应用,适用于大规模高维向量数据的存储和检索。

4.4.3 实时更新与动态调整

向量数据库需要支持实时数据更新和动态索引调整,以适应不断变化的数据和查询需求。实现这些功能的技术包括在线学习、增量索引更新和动态负载均衡等。

如有帮助,请多关注
TeahLead KrisChang,10+年的互联网和人工智能从业经验,10年+技术和业务团队管理经验,同济软件工程本科,复旦工程管理硕士,阿里云认证云服务资深架构师,上亿营收AI产品业务负责人。